<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Cole Heard</title>
    <description>The latest articles on DEV Community by Cole Heard (@coleheard).</description>
    <link>https://dev.to/coleheard</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%2F1032056%2Fff1c52b5-3469-4b46-b3ab-6f9fc3236a5d.PNG</url>
      <title>DEV Community: Cole Heard</title>
      <link>https://dev.to/coleheard</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/coleheard"/>
    <language>en</language>
    <item>
      <title>Input Validation - Terraform Tips &amp; Tricks</title>
      <dc:creator>Cole Heard</dc:creator>
      <pubDate>Sun, 10 Mar 2024 00:20:11 +0000</pubDate>
      <link>https://dev.to/coleheard/input-validation-terraform-tips-tricks-3ndl</link>
      <guid>https://dev.to/coleheard/input-validation-terraform-tips-tricks-3ndl</guid>
      <description>&lt;p&gt;Terraform &lt;a href="https://developer.hashicorp.com/terraform/language/modules" rel="noopener noreferrer"&gt;modules&lt;/a&gt; are a great way to follow D.R.Y. development principles. I’ve written about D.R.Y. in the past, but to sum it up briefly: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The DRY (“Don't Repeat Yourself”) principle follows the idea of every logic duplication being eliminated by abstraction. This means that during the development process we should avoid writing repetitive duplicated code as much as possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Writing a module for use by others on your team or the community at large can present some challenges. &lt;/p&gt;

&lt;p&gt;If someone isn't familiar with my code - they may input a value that I did not account for. &lt;/p&gt;

&lt;p&gt;Incorrect input can cause deployment failure or, even more frightening, deploy an incorrect configuration without being immediately apparent. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExdTJ6bXE1NjY4OWV5NjlzbzVqMzJmamxjdzVmMWhqN3Nuc2UxbHIwbiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/55itGuoAJiZEEen9gg/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExdTJ6bXE1NjY4OWV5NjlzbzVqMzJmamxjdzVmMWhqN3Nuc2UxbHIwbiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/55itGuoAJiZEEen9gg/giphy.gif" alt="Ralph Wiggum - Danger"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this post, I’ll be covering some of the methods I’ve used to enforce input standards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Table of Contents
&lt;/h3&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Structured Types&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation Blocks&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Going Further with Functions&lt;/li&gt;
&lt;li&gt;Adding For Expressions&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Wrapping Up&lt;/strong&gt;
&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Structured Types
&lt;/h2&gt;

&lt;p&gt;You'll see the most basic form of type enforcement everywhere you look. &lt;/p&gt;

&lt;p&gt;The variable below enforces the input standards with the type argument.&lt;/p&gt;

&lt;p&gt;This variable will only accept a &lt;strong&gt;bool&lt;/strong&gt;, true or false.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"validation_environment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bool&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Set as true to enable validation environment."&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;The input must be a number.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"maximum_sessions_allowed"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The maximum number of concurrent sessions per host."&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;You can nest and combine these types using basic Terraform syntax. In the example below, Terraform must receive a list. Even if the list only contains a single string, it is a list of one string.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"included_location_ids"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A list of Named Location IDs that will this policy will apply against."&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"All"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;In the next example, the type is defined as a map of objects. We've added a few other conditions as well - Each object must have &lt;strong&gt;4 key-value pairs&lt;/strong&gt; (&lt;strong&gt;KPV&lt;/strong&gt;). Each key must match the defined name, "app_name" or "local_path". Each KPV value is also type defined: string, number, bool, etc.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"application_map"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;app_name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;local_path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;aad_group&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;cmd_argument&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A map of all applications and their metadata."&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Lets reconsider enforcing the cmd_argument KPV from the object.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"application_map"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;app_name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;local_path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;aad_group&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A map of all applications and their metadata."&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;This change did remove the enforcement of &lt;strong&gt;cmd_argument&lt;/strong&gt;, but it also barred its presence completely. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"application_map"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;app_name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;local_path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;aad_group&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;cmd_argument&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A map of all applications and their metadata."&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;optional&lt;/strong&gt; modifier  allows &lt;strong&gt;cmd_argument&lt;/strong&gt; to be included within the object, but it won't reject an object without it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Validation Block
&lt;/h2&gt;

&lt;p&gt;The condition argument succeeds if it evaluates to &lt;strong&gt;true&lt;/strong&gt;. If the statement evaluates to &lt;strong&gt;false&lt;/strong&gt;, Terraform cancels the operation and outputs the string value of the &lt;strong&gt;error_message&lt;/strong&gt; argument. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"stars"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Please rate your sanctification with my module. Select the number of stars, 1 through 5."&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stars&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stars&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Please select a number of stars 1 through 5."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Going Further with Functions
&lt;/h3&gt;

&lt;p&gt;We can expand the validation block use with Terraform &lt;a href="https://developer.hashicorp.com/terraform/language/functions" rel="noopener noreferrer"&gt;&lt;strong&gt;functions&lt;/strong&gt;&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;In the example below, the &lt;a href="https://developer.hashicorp.com/terraform/language/functions" rel="noopener noreferrer"&gt;&lt;strong&gt;anytrue&lt;/strong&gt;&lt;/a&gt; function evaluates the input against each statement. If any of the three statements within evaluate to &lt;strong&gt;true&lt;/strong&gt;, Terraform will proceed.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"primary_color"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The primary color of your choice!"&lt;/span&gt;
  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;anytrue&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary_color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"blue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary_color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary_color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"yellow"&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The var.primary_color input is incorrect. Please select blue, red, or yellow."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Did you notice the &lt;a href="https://developer.hashicorp.com/terraform/language/functions" rel="noopener noreferrer"&gt;&lt;strong&gt;lower&lt;/strong&gt;&lt;/a&gt; function was used here as well? We don't care if the user inputs "BLUE" or "blue" do we? They're both valid primary colors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding For Expressions
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/expressions/for" rel="noopener noreferrer"&gt;For Expressions&lt;/a&gt; can be used alongside functions to build more thorough validation rules. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Anytrue evaluate every string as it loops each of the ingested list's values.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The result of each loop is compiled into a new list of bools.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally, the list of bools is evaluated against the &lt;a href="https://developer.hashicorp.com/terraform/language/functions" rel="noopener noreferrer"&gt;&lt;strong&gt;alltrue&lt;/strong&gt;&lt;/a&gt; function. If any value in the list is &lt;strong&gt;false&lt;/strong&gt;, the condition will fail and trigger error_message.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"excluded_platforms"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The policy will enforce if the sign-in comes from the listed device platform(s)."&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;alltrue&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;excluded_platforms&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;anytrue&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"all"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"android"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"iOS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"linux"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"macOS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"windows"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"windowsPhone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"unknownFutureValue"&lt;/span&gt;
      &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Invalid input for included_platforms. The list may only contain the following value(s): none, all, android, iOS, linux, macOS, windows, windowsPhone or unknownFutureValue."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


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

&lt;/div&gt;




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

&lt;p&gt;Validation blocks and structured types are both excellent tools for input validation and consistency.&lt;/p&gt;

&lt;p&gt;Mix and match both techniques for the best outcome. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.reactiongifs.com%2Fwp-content%2Fuploads%2F2013%2F07%2Fralph-wave.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.reactiongifs.com%2Fwp-content%2Fuploads%2F2013%2F07%2Fralph-wave.gif" alt="Ralph Wiggum - Goodbye"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>terraform</category>
      <category>devops</category>
      <category>infrastructureascode</category>
    </item>
    <item>
      <title>GitHub Actions - Automated Terraform-docs</title>
      <dc:creator>Cole Heard</dc:creator>
      <pubDate>Sat, 16 Sep 2023 22:30:36 +0000</pubDate>
      <link>https://dev.to/coleheard/github-actions-automated-terraform-docs-5ca5</link>
      <guid>https://dev.to/coleheard/github-actions-automated-terraform-docs-5ca5</guid>
      <description>&lt;p&gt;Earlier this year I wrote about the challenges I faced &lt;a href="https://dev.to/coleheard/writing-a-terraform-module-3md0"&gt;creating a Terraform module&lt;/a&gt;. I mentioned then that I was leveraging &lt;a href="https://terraform-docs.io/" rel="noopener noreferrer"&gt;terraform-docs&lt;/a&gt; and &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; to automate documentation, but a full workflow walkthrough was out of that post's scope. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This post, however, is entirely focused on automated documentation.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;If you're looking for an easy way to save time, enforce documentation formatting standards, or ensure documentation is kept up-to-date - &lt;a href="https://github.com/ColeHeard/githubactions-pipeline-gallery/blob/main/samples/automation/autodoc.yaml" rel="noopener noreferrer"&gt;my autodoc workflow&lt;/a&gt; is a great place to start.&lt;/p&gt;






&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Workflow&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Repo clone&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Generating Documentation&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Action arguments&lt;/li&gt;
&lt;li&gt;Markdown formatting&lt;/li&gt;
&lt;li&gt;Resulting output&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Premerge Review&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
Echoing contents

&lt;ul&gt;
&lt;li&gt;Escape characters&lt;/li&gt;
&lt;li&gt;Multiline string&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Pull request comment&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;&lt;strong&gt;Wrapping Up&lt;/strong&gt;&lt;/li&gt;

&lt;/ul&gt;






&lt;h2&gt;
  
  
  The Workflow
&lt;/h2&gt;

&lt;p&gt;The workflow starts by defining a name, a start condition, and a host that will execute the job.&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Autodoc&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Workflow&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Terraform-docs"&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tfdocs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a pull request, GitHub Actions will provision a Ubuntu VM with the latest OS version considered &lt;a href="https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources" rel="noopener noreferrer"&gt;"stable"&lt;/a&gt; by GitHub.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: The &lt;code&gt;-latest&lt;/code&gt; runner images are the latest stable images that GitHub provides, and might not be the most recent version of the operating system available from the operating system vendor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Repo clone
&lt;/h3&gt;

&lt;p&gt;Now that the job has been defined, the &lt;a href="https://github.com/actions/checkout" rel="noopener noreferrer"&gt;first step&lt;/a&gt; will clone the repository down to the runner. More specifically, it will pull the PR head from &lt;a href="https://docs.github.com/en/actions/learn-github-actions/contexts" rel="noopener noreferrer"&gt;Actions context&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pull request checkout&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.ref }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a local copy of the code now available, the runner can update the documentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Generating documentation
&lt;/h2&gt;

&lt;p&gt;This step uses the &lt;a href="https://github.com/terraform-docs/gh-actions" rel="noopener noreferrer"&gt;terraform-docs action&lt;/a&gt; to update the README.md.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;README.md generation&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-docs/gh-actions@main&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tfdocs&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;config-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-docs.yaml&lt;/span&gt;
        &lt;span class="na"&gt;working-dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
        &lt;span class="na"&gt;output-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;README.md&lt;/span&gt;
        &lt;span class="na"&gt;output-method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inject&lt;/span&gt;
        &lt;span class="na"&gt;git-push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd prefer to install the CLI tool on the runner and execute the commands directly from the shell, other &lt;a href="https://terraform-docs.io/user-guide/installation/" rel="noopener noreferrer"&gt;installation options&lt;/a&gt; exist. &lt;/p&gt;

&lt;h3&gt;
  
  
  Action arguments
&lt;/h3&gt;

&lt;p&gt;The terraform-docs action accepts many &lt;a href="https://github.com/terraform-docs/gh-actions/blob/main/README.md" rel="noopener noreferrer"&gt;arguments&lt;/a&gt;, but the five below are used by this workflow. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;config-file&lt;/strong&gt; accepts the &lt;a href="https://terraform-docs.io/user-guide/configuration" rel="noopener noreferrer"&gt;terraform-docs config file&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;working-dir&lt;/strong&gt; specifies the location of the terraform files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;output-file&lt;/strong&gt; defines the name of the generated/updated doc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;output-method&lt;/strong&gt; accepts print, replace, or inject. If other content (e.g. content not generated by terraform-docs) exists within the README.md file, you'll want to use &lt;strong&gt;inject&lt;/strong&gt;. Inject updates the existing file and places the newly created documentation between the hardcoded delimiters &lt;code&gt;&amp;lt;!-- BEGIN_TF_DOCS --&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;!-- END_TF_DOCS --&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;git-push&lt;/strong&gt; is set to "true" and adds the newly updated document to the pull request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Markdown formatting
&lt;/h3&gt;

&lt;p&gt;GitHub understands &lt;a href="https://www.markdownguide.org/" rel="noopener noreferrer"&gt;markdown&lt;/a&gt; formatting in readme files, posts, and comments. The terraform-docs.yaml file specifies markdown output to take advantage of this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;formatter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;markdown&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;table"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can find the full example &lt;a href="https://github.com/ColeHeard/example-module-hcl/blob/main/terraform-docs.yaml" rel="noopener noreferrer"&gt;terraform-docs.yaml file&lt;/a&gt; here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resulting output
&lt;/h3&gt;

&lt;p&gt;This is what the &lt;a href="https://github.com/ColeHeard/example-module-hcl#readme" rel="noopener noreferrer"&gt;README.md file&lt;/a&gt; looks like after the pull request is merged:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffrmsjeyj0ikkmdig6pjc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffrmsjeyj0ikkmdig6pjc.png" alt="Readme file"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Premerge Review
&lt;/h2&gt;

&lt;p&gt;Now that the README.md has been updated, approvers will want to review the changes prior to merging branches. &lt;/p&gt;

&lt;p&gt;The next two steps will post updated content to pull request review thread for the approver's convenience. &lt;/p&gt;
&lt;h3&gt;
  
  
  Echoing contents
&lt;/h3&gt;

&lt;p&gt;The output step echoes the file content into a standard GitHub environmental variable, &lt;a href="https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables" rel="noopener noreferrer"&gt;&lt;strong&gt;github_env&lt;/strong&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Output README.md&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;output&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;echo 'readme&amp;lt;&amp;lt;EOF' &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
        &lt;span class="s"&gt;echo "$(&amp;lt;README.md)" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
        &lt;span class="s"&gt;echo 'EOF' &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Escape characters
&lt;/h4&gt;

&lt;p&gt;Streaming output of the file into a variable will strip the content of &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html" rel="noopener noreferrer"&gt;"escape characters"&lt;/a&gt;, including &lt;strong&gt;newline&lt;/strong&gt;. To preserve these special characters, &lt;strong&gt;$(&amp;lt;README.md)&lt;/strong&gt; is wrapped in double quotes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&amp;lt;README.md&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Multiline strings
&lt;/h4&gt;

&lt;p&gt;Action's requirements surrounding environmental variables restrict the use of &lt;a href="https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings" rel="noopener noreferrer"&gt;multiline strings&lt;/a&gt;. "EOF" (end of file) is used as the delimiter to circumvent this restriction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'readme&amp;lt;&amp;lt;EOF'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$GITHUB_ENV&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&amp;lt;README.md&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$GITHUB_ENV&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'EOF'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$GITHUB_ENV&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The updated markdown content can now be used in the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pull request comment
&lt;/h3&gt;

&lt;p&gt;The last step in the workflow creates a comment on the pull request thread in GitHub with the &lt;a href="https://github.com/actions/github-script" rel="noopener noreferrer"&gt;script action&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.blog/2022-10-18-introducing-fine-grained-personal-access-tokens-for-github/" rel="noopener noreferrer"&gt;A fine-grained token&lt;/a&gt; grants access to the repository. It is stored as the secret &lt;strong&gt;GH_TOKEN&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pull request comment&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;comment&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v6&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_TOKEN }}&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;const output = `Terraform-docs has updated the README.md. &lt;/span&gt;

          &lt;span class="s"&gt;${process.env.readme}`&lt;/span&gt;
          &lt;span class="s"&gt;github.rest.issues.createComment({&lt;/span&gt;
            &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
            &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
            &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
            &lt;span class="s"&gt;body: output&lt;/span&gt;
          &lt;span class="s"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The markdown stored in the previous step is referenced with &lt;strong&gt;${process.env.readme}&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;After the job is completed, this is what the comment looks like:&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsruk2v3xce5tds31ed1j.png" alt="Pull request comment"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The job is complete and the documentation has been updated! If you'd like to test this workflow yourself, fork and modify my &lt;a href="https://github.com/ColeHeard/example-module-hcl" rel="noopener noreferrer"&gt;example module&lt;/a&gt; - Configure the repository permissions, modify a new branch, and kick off a pull request to see it in action.&lt;/p&gt;

&lt;p&gt;If you're interested in learning more about terraform-docs, checkout their &lt;a href="https://terraform-docs.io/user-guide/introduction/" rel="noopener noreferrer"&gt;user guide&lt;/a&gt; or drop by their &lt;a href="https://slack.terraform-docs.io/" rel="noopener noreferrer"&gt;slack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for taking the time read this post!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.reactiongifs.com%2Fwp-content%2Fuploads%2F2013%2F07%2Fralph-wave.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.reactiongifs.com%2Fwp-content%2Fuploads%2F2013%2F07%2Fralph-wave.gif" alt="Ralph Wiggum - Goodbye"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>automation</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
    <item>
      <title>GitHub Actions - Azure Terraform CI/CD</title>
      <dc:creator>Cole Heard</dc:creator>
      <pubDate>Wed, 05 Jul 2023 22:45:07 +0000</pubDate>
      <link>https://dev.to/coleheard/github-actions-azure-terraform-cicd-15dj</link>
      <guid>https://dev.to/coleheard/github-actions-azure-terraform-cicd-15dj</guid>
      <description>&lt;p&gt;Terraform is my preferred tool for Azure resource creation. I still run some Terraform commands from my local shell, but I've made a real effort to execute the bulk of my changes with &lt;a href="https://github.com/features/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;This post will detail my &lt;strong&gt;basic&lt;/strong&gt; Azure Terraform pipeline. If you're looking for more advanced content, this is not the article for you. &lt;/p&gt;

&lt;p&gt;First, I will highlight some workflow prerequisites. Once those have been covered, I will walkthrough the pipeline, step-by-step.  &lt;/p&gt;

&lt;p&gt;Take a seat and let's get started.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/RwLDkna2fN3fG/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/RwLDkna2fN3fG/giphy.gif" alt="Homer - Take a Seat"&gt;&lt;/a&gt;&lt;/p&gt;






&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Workflow&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Getting Started&lt;/li&gt;
&lt;li&gt;Azure Authentication&lt;/li&gt;
&lt;li&gt;Access to Private Repositories&lt;/li&gt;
&lt;li&gt;Terraform Prep&lt;/li&gt;
&lt;li&gt;Checkov&lt;/li&gt;
&lt;li&gt;More Terraform&lt;/li&gt;
&lt;li&gt;Checking the Plan&lt;/li&gt;
&lt;li&gt;Pull Request Comment&lt;/li&gt;
&lt;li&gt;Terraform Apply&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;strong&gt;Wrapping up&lt;/strong&gt;&lt;/li&gt;

&lt;/ul&gt;






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

&lt;p&gt;The workflow does not exist in a vacuum - there are a few things that need to be configured outside of the workflow itself.   &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branch protection:&lt;/strong&gt; &lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule#creating-a-branch-protection-rule" rel="noopener noreferrer"&gt;Enforced Branch protection&lt;/a&gt; requires the use of pull requests and merges. The code will always be merged from another branch to &lt;em&gt;main&lt;/em&gt;, the pull request will detail the specific changes to be made, and the &lt;em&gt;main&lt;/em&gt; branch will more reliably reflect the current state of the environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Token:&lt;/strong&gt; &lt;a href="https://github.blog/2022-10-18-introducing-fine-grained-personal-access-tokens-for-github/" rel="noopener noreferrer"&gt;A fine-grained token&lt;/a&gt; grants access to private repositories. The token is stored as a secret for later use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Azure Credentials:&lt;/strong&gt; An &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals?tabs=browser" rel="noopener noreferrer"&gt;app registration&lt;/a&gt; is used to authenticate the runner to Azure. The app registration's associated client secret - along with the subscription, tenant, and management endpoint are stored as a GitHub secret (in JSON syntax). &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;                                   
    &lt;/span&gt;&lt;span class="nl"&gt;"clientId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345678-1234-5678-9012-345678901234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"clientSecret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0000000000000000000000000000000000000000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"subscriptionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1010101-0101-0101-0101-010101010101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tenantId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ABCDEFG-HIJK-LMNO-PQRS-123456789012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"managementEndpointUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://management.core.windows.net/"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;


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

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Azure Storage Account:&lt;/strong&gt; This is an Azure focused project, so an &lt;a href="https://developer.hashicorp.com/terraform/language/settings/backends/azurerm" rel="noopener noreferrer"&gt;azurerm backend&lt;/a&gt; seemed appropriate. An Azure Storage Account was created to store Terraform's statefile. The app registration's service principal has contributor rights to the storage account - Terraform will authenticate with the same secret stored above (more on that later).&lt;/p&gt;




&lt;h2&gt;
  
  
  The Workflow
&lt;/h2&gt;

&lt;p&gt;Now that the prerequisites have been addressed, we will dissect the pipeline, &lt;a href="https://github.com/ColeHeard/githubactions-pipeline-gallery/blob/main/samples/azure/tfpipeline.yaml" rel="noopener noreferrer"&gt;tfpipeline.yaml&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting Started
&lt;/h3&gt;

&lt;p&gt;The workflow will only begin once a trigger condition has been met, as described by &lt;strong&gt;on:&lt;/strong&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic&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;Pipeline"&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;terraform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Terraform&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Ubuntu'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;Self-hosted&lt;/span&gt;

    &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;This workflow is waiting for one of two events to occur:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A pull request is opened.&lt;/li&gt;
&lt;li&gt;A push is made to the main branch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On these conditions, the &lt;a href="https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners" rel="noopener noreferrer"&gt;self-hosted runner&lt;/a&gt; starts the &lt;strong&gt;Job&lt;/strong&gt; as defined below.&lt;/p&gt;

&lt;p&gt;The checkout step clones the repository down to the runner.&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;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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Code Checkout&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Azure Authentication
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/marketplace/actions/azure-login" rel="noopener noreferrer"&gt;Azure Login Action&lt;/a&gt; authenticates with the Azure JSON secret.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure Authentication&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;login&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;creds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZJSON }}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;parse&lt;/strong&gt; step uses &lt;a href="https://github.com/jqlang/jq" rel="noopener noreferrer"&gt;jq&lt;/a&gt; to parse the Azure JSON. The key values are echoed to environmental variables for use by Terraform. &lt;/p&gt;

&lt;p&gt;The variables are ultimately passed to &lt;strong&gt;$GITHUB_ENV&lt;/strong&gt;, one of GitHub Actions &lt;a href="https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables" rel="noopener noreferrer"&gt;default environmental variables&lt;/a&gt;.   &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JSON Parse&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;parse&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;AZJSON&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZJSON }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;ARM_CLIENT_ID=$(echo $AZJSON | jq -r '.["clientId"]')&lt;/span&gt;
          &lt;span class="s"&gt;ARM_CLIENT_SECRET=$(echo $AZJSON | jq -r '.["clientSecret"]')&lt;/span&gt;
          &lt;span class="s"&gt;ARM_TENANT_ID=$(echo $AZJSON | jq -r '.["tenantId"]')&lt;/span&gt;
          &lt;span class="s"&gt;ARM_SUBSCRIPTION_ID=$(echo $AZJSON | jq -r '.["subscriptionId"]')&lt;/span&gt;
          &lt;span class="s"&gt;echo ARM_CLIENT_ID=$ARM_CLIENT_ID &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
          &lt;span class="s"&gt;echo ARM_CLIENT_SECRET=$ARM_CLIENT_SECRET &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
          &lt;span class="s"&gt;echo ARM_TENANT_ID=$ARM_TENANT_ID &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
          &lt;span class="s"&gt;echo ARM_SUBSCRIPTION_ID=$ARM_SUBSCRIPTION_ID &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Access to Private Repositories
&lt;/h3&gt;

&lt;p&gt;The GitHub token is streamed to &lt;a href="https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html" rel="noopener noreferrer"&gt;.netrc&lt;/a&gt;. Git is then configured to use https for the logon. These git changes provide access to the private registry configured within the same GitHub org.&lt;/p&gt;

&lt;p&gt;This step also disables git's detached head warnings.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub Token&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;token&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GHTOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "machine github.com login x password ${TOKEN}" &amp;gt; ~/.netrc&lt;/span&gt;
          &lt;span class="s"&gt;git config --global url."https://github.com/".insteadOf "git://github.com/"&lt;/span&gt;
          &lt;span class="s"&gt;git config --global advice.detachedHead false&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Terraform Prep
&lt;/h3&gt;

&lt;p&gt;Terraform is installed and initialized. &lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;azurerm&lt;/strong&gt; backend is configured to use environmental variables created during the parse step. Only a single secret is maintained for &lt;em&gt;all&lt;/em&gt; Azure authentication.  &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Terraform&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v2.0.3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;terraform_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.3.5&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Init&lt;/span&gt;
        &lt;span class="na"&gt;id&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terraform init&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Checkov
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.checkov.io" rel="noopener noreferrer"&gt;Checkov&lt;/a&gt; is an open-source static code analysis tool. &lt;br&gt;
The tool compares the code to defined policy - the policies can be out-of-the-box security checks or custom .py or .yaml files.&lt;/p&gt;

&lt;p&gt;Here &lt;a href="https://pypi.org/project/pip/" rel="noopener noreferrer"&gt;Pip&lt;/a&gt; installs Checkov.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Checkov&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkov&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pip install checkov&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Notice the &lt;strong&gt;if:&lt;/strong&gt; expression. These steps only run if the trigger event was a pull request.&lt;/p&gt;

&lt;p&gt;Checkov will now run. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkov Static Test&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;static&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;checkov -d . --download-external-modules true&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Checkov should run after Terraform init; any modules called by Terraform are installed during init and we'll want Checkov to test their code as well.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  More Terraform
&lt;/h3&gt;

&lt;p&gt;The next few steps are Terraform staples. We run format, validate, and plan.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Format&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fmt&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform fmt -check -recursive&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Validate&lt;/span&gt;
        &lt;span class="na"&gt;id&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform validate -no-color&lt;/span&gt;     

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Plan&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tplan&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
          &lt;span class="na"&gt;TF_VAR_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.example_secret&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;terraform plan -no-color&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Checking the Plan
&lt;/h3&gt;

&lt;p&gt;This step requires another Terraform plan run. The previous plan did not output to a file - the console output from &lt;strong&gt;tplan&lt;/strong&gt; will be used later. &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkov Plan Test&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cplan&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
           &lt;span class="na"&gt;TF_VAR_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.example_secret&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;terraform plan --out plan.tfplan&lt;/span&gt;
            &lt;span class="s"&gt;terraform show -json plan.tfplan &amp;gt; tfplan.json&lt;/span&gt;
            &lt;span class="s"&gt;ls&lt;/span&gt;
            &lt;span class="s"&gt;checkov -f tfplan.json --framework terraform_plan&lt;/span&gt;


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

&lt;/div&gt;
&lt;h3&gt;
  
  
  Pull Request Comment
&lt;/h3&gt;

&lt;p&gt;This step uses the &lt;a href="https://github.com/actions/github-script" rel="noopener noreferrer"&gt;GitHub Script&lt;/a&gt; action. &lt;/p&gt;

&lt;p&gt;A comment will be created on the pull request. The outcome of many previous steps is displayed for review. Additionally, the full output of the Terraform plan is available as well. &lt;/p&gt;

&lt;p&gt;Much of this step's code was borrowed from the &lt;a href="https://github.com/marketplace/actions/hashicorp-setup-terraform" rel="noopener noreferrer"&gt;Setup Terraform&lt;/a&gt; Action documentation.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pull Request Comment&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;comment&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v3&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;TPLAN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;terraform&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;steps.tplan.outputs.stdout&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GHTOKEN }}&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;const output = `&lt;/span&gt;
            &lt;span class="s"&gt;### Pull Request Information&lt;/span&gt;
            &lt;span class="s"&gt;Please review this pull request. Merging the PR will run Terraform Apply with the plan detailed below.&lt;/span&gt;

            &lt;span class="s"&gt;#### Terraform Checks&lt;/span&gt;
            &lt;span class="s"&gt;Init: \`${{ steps.init.outcome }}\`&lt;/span&gt;
            &lt;span class="s"&gt;Format: \`${{ steps.fmt.outcome }}\`&lt;/span&gt;
            &lt;span class="s"&gt;Validation: \`${{ steps.validate.outcome }}\`&lt;/span&gt;
            &lt;span class="s"&gt;Plan: \`${{ steps.tplan.outcome }}\`&lt;/span&gt;

            &lt;span class="s"&gt;#### Checkov&lt;/span&gt;
            &lt;span class="s"&gt;Static: \`${{ steps.static.outcome }}\`&lt;/span&gt;
            &lt;span class="s"&gt;Plan: \`${{ steps.cplan.outcome }}\`&lt;/span&gt;

            &lt;span class="s"&gt;&amp;lt;details&amp;gt;&amp;lt;summary&amp;gt;Plan File&amp;lt;/summary&amp;gt;&lt;/span&gt;

            &lt;span class="s"&gt;\`\`\`${process.env.TPLAN}\`\`\`&lt;/span&gt;

            &lt;span class="s"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;

            &lt;span class="s"&gt;`&lt;/span&gt;
            &lt;span class="s"&gt;github.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;body: output&lt;/span&gt;
            &lt;span class="s"&gt;})&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;This is what the comment looks like:&lt;/p&gt;

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

&lt;p&gt;If the trigger event was a pull request, the workflow ends here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform Apply
&lt;/h3&gt;

&lt;p&gt;The workflow only runs Terraform apply when the push occurs on the main branch. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Apply&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apply&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main' &amp;amp;&amp;amp; github.event_name == 'push'&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;TF_VAR_domain_pass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.DOMAIN_JOIN_PASS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;TF_VAR_local_pass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.LOCAL_ADMIN_PASS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;TF_VAR_workspace_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.LA_WORKSPACE_KEY&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform apply -auto-approve&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;That's all. The workflow tour is finished.&lt;/p&gt;

&lt;p&gt;Here is what it looks like all put together:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;I've found a lot of value deploying resources with this workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resources will be created using the same method, every deployment.&lt;/li&gt;
&lt;li&gt;With Branch protection enabled, approvals can easily be incorporated into the deployment process.&lt;/li&gt;
&lt;li&gt;Deploying resources with a purpose-built service principal follows the principal of least privilege.&lt;/li&gt;
&lt;li&gt;Automated policy checks ensures that they're always being run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you enjoyed this article, take a look at my &lt;a href="https://dev.to/coleheard/github-actions-sharepoint-framework-cicd-kan"&gt;SharePoint Framework pipeline&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Until next time!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.reactiongifs.com%2Fwp-content%2Fuploads%2F2013%2F07%2Fralph-wave.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.reactiongifs.com%2Fwp-content%2Fuploads%2F2013%2F07%2Fralph-wave.gif" alt="Ralph Wiggum - Goodbye"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>terraform</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>GitHub Actions - SharePoint Framework CI/CD</title>
      <dc:creator>Cole Heard</dc:creator>
      <pubDate>Thu, 09 Mar 2023 12:23:40 +0000</pubDate>
      <link>https://dev.to/coleheard/github-actions-sharepoint-framework-cicd-kan</link>
      <guid>https://dev.to/coleheard/github-actions-sharepoint-framework-cicd-kan</guid>
      <description>&lt;p&gt;The SharePoint Framework (SPFx) is Microsoft's development model for creating custom web parts in SharePoint Online. It can greatly expand SharePoint's capabilities - something out-of-the-box SharePoint Online &lt;em&gt;desperately&lt;/em&gt; needs.&lt;/p&gt;

&lt;p&gt;This guide will walk you through the creation of a &lt;strong&gt;basic&lt;/strong&gt; CI/CD pipeline for SharePoint development. The GitHub Action workflow will build and deploy custom SharePoint web parts directly to your tenant.&lt;/p&gt;






&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why write SPFx web parts?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Active Directory prerequisites&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Service Account&lt;/li&gt;
&lt;li&gt;Conditional Access&lt;/li&gt;
&lt;li&gt;Application Consent&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Pipeline walkthrough&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Workflow triggers&lt;/li&gt;
&lt;li&gt;Running the job&lt;/li&gt;
&lt;li&gt;Setup steps&lt;/li&gt;
&lt;li&gt;Deployment steps&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;strong&gt;Wrapping up&lt;/strong&gt;&lt;/li&gt;

&lt;/ul&gt;






&lt;h2&gt;
  
  
  Why write SPFx web parts?
&lt;/h2&gt;

&lt;p&gt;You can build some useful &lt;a href="https://github.com/OlivierCC/spfx-40-fantastics" rel="noopener noreferrer"&gt;web parts&lt;/a&gt; and &lt;a href="https://github.com/pnp/sp-dev-fx-webparts" rel="noopener noreferrer"&gt;widgets&lt;/a&gt; with SPFx. &lt;/p&gt;

&lt;p&gt;It can also add features Microsoft should have included standard.&lt;/p&gt;

&lt;p&gt;For instance - SharePoint Online does not have a dynamically updating table of contents. You have to &lt;a href="https://pnp.github.io/blog/post/create-a-table-of-contents-on-sharepoint-modern-pages/" rel="noopener noreferrer"&gt;type it out manually&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.gifer.com%2F6iot.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.gifer.com%2F6iot.gif" alt="Ralph Wiggum - 404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want a dynamic table of contents you need a &lt;a href="https://answers.microsoft.com/en-us/msoffice/forum/all/toc-webpart-cannot-be-seen-in-sharepoint-modern/7482e22b-0a4f-4821-9c27-e33fe6192ace" rel="noopener noreferrer"&gt;custom web part&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Azure Active Directory prerequisites
&lt;/h2&gt;

&lt;p&gt;Before building the pipeline, a few Azure Active Directory prerequisites must be met.&lt;/p&gt;

&lt;h3&gt;
  
  
  Service Account
&lt;/h3&gt;

&lt;p&gt;A service account (SA) with the appropriate permissions in SharePoint is required to deploy the .sspkg to the application catalog. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If targeting a single site, the account will need &lt;em&gt;Site Collection Administrator&lt;/em&gt;. &lt;/li&gt;
&lt;li&gt;If targeting the tenant as a whole, the account will likely need &lt;em&gt;SharePoint Administrator&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unfortunately, we cannot use a service principal/application registration for SharePoint.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Attention: Currently, SharePoint does not support authentication using Azure AD App ID and Secret. CLI for Microsoft 365 commands that call the SharePoint APIs will fail while logged in to Microsoft 365 using a Secret.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;See the &lt;a href="https://pnp.github.io/cli-microsoft365/user-guide/connecting-office-365/#log-in-using-a-secret" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; for additional information on this limitation. &lt;/p&gt;

&lt;h3&gt;
  
  
  Conditional Access
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/conditional-access/overview" rel="noopener noreferrer"&gt;Conditional Access&lt;/a&gt; rules should be configured allowing the SA access only if certain conditions are met.&lt;/p&gt;

&lt;p&gt;I would highly recommend building a policy leveraging some or all of these rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identity (i.e. apply only to the SA)&lt;/li&gt;
&lt;li&gt;Trusted IP ranges&lt;/li&gt;
&lt;li&gt;Runner OS&lt;/li&gt;
&lt;li&gt;Cloud Application (31359c7f-bd7e-475c-86db-fdb8c937548e)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Application Consent
&lt;/h3&gt;

&lt;p&gt;The SA will need &lt;a href="https://pnp.github.io/powershell/articles/authentication.html" rel="noopener noreferrer"&gt;application consent&lt;/a&gt; to login to the PnP Management Shell. Run the PowerShell commands below in the Azure Cloud Shell.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Install-Module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PnP.PowerShell&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Register-PnPManagementShellAccess&lt;/span&gt;&lt;span class="w"&gt;


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

&lt;/div&gt;

&lt;p&gt;You may not be able to perform these actions in a &lt;a href="https://developer.microsoft.com/en-us/microsoft-365/dev-program" rel="noopener noreferrer"&gt;development M365 tenant&lt;/a&gt; - the developer instance prohibits the use of the Cloud Shell.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pipeline walkthrough
&lt;/h2&gt;

&lt;p&gt;Now that the prerequisites have been addressed, we will dissect the pipeline, &lt;a href="https://github.com/ColeHeard/githubactions-pipeline-gallery/blob/11955db66727a04bcca5f8111f56965fcb659dcd/samples/sharepoint/spfxpipeline.yaml" rel="noopener noreferrer"&gt;spfxpipeline.yaml&lt;/a&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Workflow triggers
&lt;/h3&gt;

&lt;p&gt;The workflow will only begin once a trigger condition has been met, as described by &lt;strong&gt;on:&lt;/strong&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;SharePoint&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Framework&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Pipeline&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Microsoft&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;365"&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;main'&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;This workflow is waiting for one of two events to occur:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A push occurs on the main branch.&lt;/li&gt;
&lt;li&gt;A "workflow_dispatch" is manually triggered - useful when testing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once triggered, the workflow will kick off a &lt;strong&gt;Job&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the job
&lt;/h3&gt;

&lt;p&gt;We start by labeling the job and selecting a runner.&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SPFx&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Deployment'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;runs-on:&lt;/strong&gt; argument defines the runner our job will execute on. &lt;/p&gt;

&lt;p&gt;In the example above, I use a &lt;a href="https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners" rel="noopener noreferrer"&gt;self-hosted runner&lt;/a&gt; provisioned within Azure. &lt;/p&gt;

&lt;p&gt;Substitute &lt;em&gt;self-hosted&lt;/em&gt; with &lt;em&gt;ubuntu-latest&lt;/em&gt; to use a &lt;a href="https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners" rel="noopener noreferrer"&gt;GitHub-hosted runner&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, &lt;strong&gt;steps:&lt;/strong&gt;  define the Job's individual actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup steps
&lt;/h3&gt;

&lt;p&gt;The first two steps are Actions. Actions are small, purpose built applets. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Action steps are defined with the &lt;strong&gt;uses:&lt;/strong&gt; keyword.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;a href="https://github.com/actions/checkout" rel="noopener noreferrer"&gt;checkout&lt;/a&gt; action is a workflow staple - it clones the repository's code down to the runner. &lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/actions/setup-node" rel="noopener noreferrer"&gt;setup-node&lt;/a&gt;  action installs and configures node. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Node.js install&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;16.x&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;The next few steps run commands directly in the runner's shell.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Run steps are defined by the &lt;strong&gt;run:&lt;/strong&gt; keyword.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One step installs &lt;a href="https://www.npmjs.com/" rel="noopener noreferrer"&gt;NPM&lt;/a&gt; (with the &lt;a href="https://docs.npmjs.com/cli/v9/commands/npm-ci" rel="noopener noreferrer"&gt;CI switch&lt;/a&gt;) and the next one installs &lt;a href="https://gulpjs.com/" rel="noopener noreferrer"&gt;Gulp&lt;/a&gt; globally. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NPM install&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gulp install&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm i -g gulp&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;The environment has been configured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment steps
&lt;/h3&gt;

&lt;p&gt;The runner can now compile our web part. The step below builds our package with two Gulp commands. &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gulp package&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;gulp bundle --ship&lt;/span&gt;
          &lt;span class="s"&gt;gulp package-solution --ship   &lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://github.com/pnp/action-cli-login" rel="noopener noreferrer"&gt;action-cli-login&lt;/a&gt; action will authenticate with the service account discussed earlier. &lt;/p&gt;

&lt;p&gt;As the SA's credentials are sensitive, the workflow will reference &lt;a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets" rel="noopener noreferrer"&gt;GitHub Action Secrets&lt;/a&gt; to inject the credentials into the pipeline as needed.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;M365 login&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;login&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnp/action-cli-login@v2.2.1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ADMIN_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;${{ secrets.ADMIN_USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;ADMIN_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;${{ secrets.ADMIN_PASSWORD }}&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;Finally, the package is deployed to the tenant's SharePoint application catalog using &lt;a href="https://github.com/pnp/action-cli-deploy" rel="noopener noreferrer"&gt;action-cli-deploy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We specify the package's relative path with &lt;strong&gt;APP_FILE_PATH&lt;/strong&gt; and grant permission to overwrite any existing deployments of the application with &lt;strong&gt;OVERWRITE&lt;/strong&gt; set to &lt;em&gt;true&lt;/em&gt;.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SharePoint deploy&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnp/action-cli-deploy@v3.0.1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;APP_FILE_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./package/example-web-part.sppkg&lt;/span&gt;
          &lt;span class="na"&gt;OVERWRITE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;


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

&lt;/div&gt;

&lt;p&gt;In production, you'd want to replace the hardcoded path and file name with a variable. &lt;/p&gt;

&lt;p&gt;The workflow has completed and the custom web part has been deployed to the tenant.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffw8lc82enjh7b6wb8z27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffw8lc82enjh7b6wb8z27.png" alt="The Application has been uploaded."&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;As I said back at the beginning of this post, this is a basic pipeline. It is a solid foundation to build upon and not much more.&lt;/p&gt;

&lt;p&gt;If you're looking for something more advanced, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integrating the workflow with your project management platform of choice.&lt;/li&gt;
&lt;li&gt;Setting up &lt;a href="https://jestjs.io/" rel="noopener noreferrer"&gt;Jest&lt;/a&gt; or &lt;a href="https://enzymejs.github.io/enzyme/" rel="noopener noreferrer"&gt;Enzyme&lt;/a&gt;  for SPFx unit testing.&lt;/li&gt;
&lt;li&gt;Building a SharePoint UAT environment to pre-stage your deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.reactiongifs.com%2Fwp-content%2Fuploads%2F2013%2F07%2Fralph-wave.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.reactiongifs.com%2Fwp-content%2Fuploads%2F2013%2F07%2Fralph-wave.gif" alt="Ralph Wiggum - Goodbye"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>githubactions</category>
      <category>microsoftcloud</category>
      <category>sharepoint</category>
    </item>
    <item>
      <title>Staying DRY - Writing a Terraform Module</title>
      <dc:creator>Cole Heard</dc:creator>
      <pubDate>Sun, 26 Feb 2023 01:05:06 +0000</pubDate>
      <link>https://dev.to/coleheard/writing-a-terraform-module-3md0</link>
      <guid>https://dev.to/coleheard/writing-a-terraform-module-3md0</guid>
      <description>&lt;p&gt;After writing my Azure Virtual Desktop (AVD) environment in HCL, I knew a few changes would be required before I could use my code in production. &lt;/p&gt;

&lt;p&gt;The project was flat and wide - almost every object had its own block of code. Sure, I took advantage of the count argument when it was a clear fit, but most objects still had a 1-to-1 relationship with a resource block.&lt;/p&gt;

&lt;p&gt;Writing a module was the obvious solution. A well-written Terraform module can be easily reused, scaled, shared, versioned, and maintained. They are also an excellent tool when building with a DRY design focus.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The DRY (“Don't Repeat Yourself”) principle follows the idea of every logic duplication being eliminated by abstraction. This means that during the development process we should avoid writing repetitive duplicated code as much as possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post will highlight the challenges I encountered while writing the module and the lessons I learned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons Learned
&lt;/h3&gt;



&lt;ul&gt;
&lt;li&gt;Mutually Exclusive Arguments&lt;/li&gt;
&lt;li&gt;Optional Resource Blocks&lt;/li&gt;
&lt;li&gt;Validating Input&lt;/li&gt;
&lt;li&gt;Resources with Complicated Relationships&lt;/li&gt;
&lt;li&gt;Expiring Timestamps&lt;/li&gt;
&lt;li&gt;
Adding Workspace Support
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Mutually Exclusive Arguments
&lt;/h2&gt;

&lt;p&gt;The first set of resources I targeted were the session hosts.&lt;/p&gt;

&lt;p&gt;The backbone of the session host, &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/windows_virtual_machine" rel="noopener noreferrer"&gt;the virtual machine&lt;/a&gt;, may use an &lt;a href="https://azuremarketplace.microsoft.com/en-us/marketplace/apps/category/compute?page=1&amp;amp;filters=windows%3Bmicrosoft" rel="noopener noreferrer"&gt;Azure Marketplace&lt;/a&gt; image in one configuration and a custom &lt;a href="https://learn.microsoft.com/en-us/azure/virtual-machines/azure-compute-gallery" rel="noopener noreferrer"&gt;Azure Compute Gallery&lt;/a&gt; shared image in another.&lt;/p&gt;

&lt;p&gt;This was a problem - the arguments for each of these image types are mutually exclusive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Some resource blocks have mutually exclusive arguments.&lt;/li&gt;
&lt;li&gt;One argument must be present or provisioning will fail.&lt;/li&gt;
&lt;li&gt;The module must support the use of both arguments.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;Using &lt;a href="https://developer.hashicorp.com/terraform/language/expressions/conditionals" rel="noopener noreferrer"&gt;conditional expressions&lt;/a&gt; and &lt;a href="https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks" rel="noopener noreferrer"&gt;dynamic blocks&lt;/a&gt;, the module can dynamically select one of the arguments and omit the other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_windows_virtual_machine"&lt;/span&gt; &lt;span class="s2"&gt;"vm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
 &lt;span class="nx"&gt;source_image_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;managed_image_id&lt;/span&gt;
 &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="s2"&gt;"source_image_reference"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;managed_image_id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"One loop, please!"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
   &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nx"&gt;publisher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;market_place_image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publisher&lt;/span&gt;
     &lt;span class="nx"&gt;offer&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;market_place_image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offer&lt;/span&gt;
     &lt;span class="nx"&gt;sku&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;market_place_image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&lt;/span&gt;
     &lt;span class="nx"&gt;version&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;market_place_image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;
     &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an Azure Compute Gallery image ID was passed to the module, var.managed_image_id would set &lt;strong&gt;source_image_id&lt;/strong&gt;. As var.managed_image_id is &lt;em&gt;not&lt;/em&gt; null, the dynamic block would not loop... or if we're being pedantic it would loop &lt;em&gt;0 times&lt;/em&gt;. This functionally omits the &lt;strong&gt;source_image_reference&lt;/strong&gt; argument.&lt;/p&gt;

&lt;p&gt;Alternatively, if var.managed_image_id was null (the variable's default) at runtime, the argument would be ignored. As the execution moves to evaluate the dynamic block below, it would find that var.managed_image_id is null and the block will loop once.&lt;/p&gt;

&lt;p&gt;This solution isn't perfect - the module will fail to provision if neither argument is made. We need to ensure the block is fed &lt;em&gt;something&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Validation can't be used as &lt;a href="https://developer.hashicorp.com/terraform/language/values/variables#custom-validation-rules" rel="noopener noreferrer"&gt;validation blocks&lt;/a&gt; cannot &lt;a href="https://github.com/hashicorp/terraform/issues/25609#issuecomment-1212592393" rel="noopener noreferrer"&gt;reference other variables&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As a bandaid, var.market_place_image was given a default value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"managed_image_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;any&lt;/span&gt;
 &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The ID of an Azure Compute Gallery image."&lt;/span&gt;
 &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"market_place_image"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The publisher, offer, sku, and version of an image in Azure's market place. Only used if var.custom_image is null."&lt;/span&gt;
 &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;publisher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"microsoftwindowsdesktop"&lt;/span&gt;
   &lt;span class="nx"&gt;offer&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"windows-10"&lt;/span&gt;
   &lt;span class="nx"&gt;sku&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"win10-22h2-ent"&lt;/span&gt;
   &lt;span class="nx"&gt;version&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"latest"&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Optional Resource Blocks
&lt;/h2&gt;

&lt;p&gt;I wanted to include support for multiple &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_machine_extension" rel="noopener noreferrer"&gt;virtual machine extensions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The DSC extension is always needed; it installs the &lt;a href="https://learn.microsoft.com/en-us/azure/virtual-desktop/agent-overview#initial-installation-process" rel="noopener noreferrer"&gt;AVD agent&lt;/a&gt; on the host and configures it interpolated host pool registration token.&lt;/p&gt;

&lt;p&gt;The domain join extension and Microsoft monitoring agent extensions, however, are entirely optional.&lt;/p&gt;

&lt;p&gt;I needed the module to skip provisioning these resources based on existing variable input.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;A resources block should not be provisioned if a condition is not met.&lt;/li&gt;
&lt;li&gt;Additional module input should be avoided.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;The module can determine if the resource block is needed using conditional expressions, &lt;a href="https://developer.hashicorp.com/terraform/language/values/locals" rel="noopener noreferrer"&gt;locals&lt;/a&gt;, and the &lt;a href="https://developer.hashicorp.com/terraform/language/meta-arguments/count" rel="noopener noreferrer"&gt;count&lt;/a&gt; argument.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;extensions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;mmaagent&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace_id&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vmcount&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_virtual_machine_extension"&lt;/span&gt; &lt;span class="s2"&gt;"mmaagent_ext"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;count&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mmaagent&lt;/span&gt;
 &lt;span class="nx"&gt;name&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%02d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-avd_mma"&lt;/span&gt;
 &lt;span class="nx"&gt;virtual_machine_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_windows_virtual_machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.*.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="nx"&gt;publisher&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Microsoft.EnterpriseCloud.Monitoring"&lt;/span&gt;
 &lt;span class="nx"&gt;type&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MicrosoftMonitoringAgent"&lt;/span&gt;
 &lt;span class="nx"&gt;type_handler_version&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.0"&lt;/span&gt;
 &lt;span class="nx"&gt;auto_upgrade_minor_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
 &lt;span class="nx"&gt;settings&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;SETTINGS&lt;/span&gt;&lt;span class="sh"&gt;
   {
     "workspaceId": "${var.workspace_id}"
   }
&lt;/span&gt;&lt;span class="no"&gt;SETTINGS
&lt;/span&gt; &lt;span class="nx"&gt;protected_settings&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;PROTECTED_SETTINGS&lt;/span&gt;&lt;span class="sh"&gt;
  {
     "workspaceKey": "${var.workspace_key}"
  }
&lt;/span&gt;&lt;span class="no"&gt;PROTECTED_SETTINGS
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we're configuring the pool's session hosts to enroll in our log analytics workspace, we'll pass the module the workspace ID. The conditional expression within local.extensions.mmaagent is set to var.vmcount. An extension is created for every virtual machine in the pool.&lt;/p&gt;

&lt;p&gt;If the workspace ID is not set, the conditional expression local.extensions.mmaagent is set to 0. The entire block will be skipped.&lt;/p&gt;




&lt;h2&gt;
  
  
  Validating Input
&lt;/h2&gt;

&lt;p&gt;As the module's input became increasingly complex, so did the input requirements. It became clear that some input validation was needed - especially if the module was to be shared with others.&lt;/p&gt;

&lt;p&gt;Fine-tuned validation can improve a module's ease of use, error checking, and reliability.&lt;/p&gt;

&lt;p&gt;Excessive validation can make a module virtually unusable.&lt;/p&gt;

&lt;p&gt;A set of variables were identified as perfect candidates for validation - three are highlighted in the solutions below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Some variables have input structure requirements.&lt;/li&gt;
&lt;li&gt;Some variables must be limited to avoid errors.&lt;/li&gt;
&lt;li&gt;Some variables only accept a small pool of values.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Solution #1
&lt;/h3&gt;

&lt;p&gt;Terraform's &lt;a href="https://developer.hashicorp.com/terraform/language/values/variables#custom-validation-rules" rel="noopener noreferrer"&gt;validation&lt;/a&gt; block can perform &lt;a href="https://developer.hashicorp.com/terraform/language/expressions/custom-conditions#input-variable-validation" rel="noopener noreferrer"&gt;custom condition checks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The int passed to the variable below must be between 0 and 99.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"vmcount"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The number of VMs requested for this pool."&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vmcount&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vmcount&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The number of VMs must be between 0 and 99."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the example below, the &lt;a href="https://developer.hashicorp.com/terraform/language/functions/lower" rel="noopener noreferrer"&gt;lower&lt;/a&gt; function is used on the value prior to evaluation.&lt;/p&gt;

&lt;p&gt;As the &lt;a href="https://developer.hashicorp.com/terraform/language/expressions/operators#equality-operators" rel="noopener noreferrer"&gt;equality operator&lt;/a&gt; judges the case of the string, it will not error due to a case mismatch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"load_balancer_type"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The method of load balancing the pool with use to distribute users across session hosts."&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DepthFirst"&lt;/span&gt;
  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;anytrue&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load_balancer_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"breadthfirst"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load_balancer_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"depthfirst"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load_balancer_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"persistent"&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The var.load_balancer_type input was incorrect. Please select breadthfirst, depthfirst, or persistent."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Solution #2
&lt;/h3&gt;

&lt;p&gt;The Variable's &lt;a href="https://developer.hashicorp.com/terraform/language/values/variables#type-constraints" rel="noopener noreferrer"&gt;type&lt;/a&gt; argument can do more than specify if a variable should be a string, a bool, or a list.&lt;/p&gt;

&lt;p&gt;In the example below, var.application_map is expecting an input that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A map of objects&lt;/li&gt;
&lt;li&gt;The objects each contain four keys named app_name, local_path, cmd_argument, aad_group.&lt;/li&gt;
&lt;li&gt;Each of the keys contain a string (or null).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"application_map"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;app_name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;local_path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;cmd_argument&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;aad_group&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A map of all applications and metadata."&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Additional Notes
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;While 99 is not &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/example-scenario/wvd/windows-virtual-desktop#azure-virtual-desktop-limitations" rel="noopener noreferrer"&gt;the hard limit&lt;/a&gt;, it is a safe number to work with - namely, to avoid NetBIOS name limitations and API throttling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember var.application_map!&lt;/strong&gt; The data structure will be discussed at length in the next section.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Resources with Complicated Relationships
&lt;/h2&gt;

&lt;p&gt;A pair of resource blocks scale differently. Despite this, our module still needs to correctly tie them together.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_desktop_application_group" rel="noopener noreferrer"&gt;Application Groups&lt;/a&gt; and &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_desktop_application" rel="noopener noreferrer"&gt;Applications&lt;/a&gt; share a complicated relationship:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A single application pool can have multiple application groups.&lt;/li&gt;
&lt;li&gt;An application group can have many applications.&lt;/li&gt;
&lt;li&gt;Each application group in the pool may have a different number of applications assigned.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/azure/virtual-desktop/rbac#desktop-virtualization-user" rel="noopener noreferrer"&gt;Permissions&lt;/a&gt; are assigned at an Application groups level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How can the module identify which resources should connect to each other?&lt;/p&gt;

&lt;p&gt;How can the module accomplish all of this, while only using the input from &lt;strong&gt;var.application_map&lt;/strong&gt;?&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Resources should dynamically connect to each other.&lt;/li&gt;
&lt;li&gt;The module should support as many configurations as it possibly can.&lt;/li&gt;
&lt;li&gt;Additional module input should be avoided.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;We use the shared Azure Active Directory (AAD) groups to assign and collect each application.  &lt;/p&gt;

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

&lt;p&gt;By organizing each application under their shared aad_group, we're able to scale both resource blocks independently while retaining the ability to logically join them.  &lt;/p&gt;

&lt;h4&gt;
  
  
  Creating Application Groups
&lt;/h4&gt;

&lt;p&gt;We make a list of each unique aad_group key value with local.aad_group_list.&lt;/p&gt;

&lt;p&gt;The local selects the aad_group &lt;a href="https://developer.hashicorp.com/terraform/language/functions/values" rel="noopener noreferrer"&gt;values&lt;/a&gt; and eliminates the duplicates with the &lt;a href="https://developer.hashicorp.com/terraform/language/functions/distinct" rel="noopener noreferrer"&gt;distinct&lt;/a&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nx"&gt;aad_group_list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_map&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;distinct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_map&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aad_group&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aad_group_desktop&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we create each of our application groups using &lt;strong&gt;for_each = toset(local.aad_group_list)&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_virtual_desktop_application_group"&lt;/span&gt; &lt;span class="s2"&gt;"app_group"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aad_group_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module will produce one application group for every unique AAD group reference.&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxf0jr6yvlfnhpyxhx1n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxf0jr6yvlfnhpyxhx1n.png" alt="Creating Red application groups"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  Creating Applications
&lt;/h4&gt;

&lt;p&gt;The application block, looping through the unaltered list of application objects, inserts the aad_group key value into application_group_id.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_virtual_desktop_application"&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applications&lt;/span&gt; &lt;span class="c1"&gt;# In this example, this local resolves to var.application_map. See additional notes for more info.&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"app_name"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;friendly_name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"app_name"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;application_group_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_virtual_desktop_application_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"aad_group"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;                         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"local_path"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;command_line_argument_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cmd_argument"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"DoNotAllow"&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Require"&lt;/span&gt;
  &lt;span class="nx"&gt;command_line_arguments&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cmd_argument"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application has successfully recreated the resource address of the correct application group.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwwplna7inxg39mujhzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwwplna7inxg39mujhzo.png" alt="Example flow with Green Application 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In short - every application will be assigned to the application group with a matching AAD group.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Notes
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Desktop pools do not require &lt;em&gt;"applications"&lt;/em&gt;. The desktop application group inherently provides the "Remote Desktop" application to all assigned members.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Local.applications will pass var.appliction_map unless it is null. If it is null, we must pass an empty map instead - null cannot be given to the for_each argument.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;applications&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_map&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_map&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tomap&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt; &lt;span class="c1"&gt;# Null is not accepted as for_each value, substituting for an empty map if null.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Expiring Timestamps
&lt;/h2&gt;

&lt;p&gt;The last section was heavy - this one is a palate cleanser.&lt;/p&gt;

&lt;p&gt;While working on this module, I needed to manually update the &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_desktop_host_pool_registration_info" rel="noopener noreferrer"&gt;registration token&lt;/a&gt;'s RF3339 timestamp several times. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExMGJhYTAwYTMyMzJhNThiZWQ0ZmQwZjk2ZDdiNTBhZWIwZWZkZTY5MCZjdD1n/ftegeAlQ32zFLEKjrk/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExMGJhYTAwYTMyMzJhNThiZWQ0ZmQwZjk2ZDdiNTBhZWIwZWZkZTY5MCZjdD1n/ftegeAlQ32zFLEKjrk/giphy.gif" alt="Ralph Wiggum - Easter"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm not updating it myself any longer - Terraform functions can handle it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;A timestamp is needed to generate a token.&lt;/li&gt;
&lt;li&gt;The token should expire as soon as possible after resources are provisioned.&lt;/li&gt;
&lt;li&gt;No manual intervention at run time is permitted.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;The RF3339 timestamp is updated each time we &lt;strong&gt;plan&lt;/strong&gt; or &lt;strong&gt;apply&lt;/strong&gt;. The module leverages two simple Terraform functions:&lt;br&gt;
&lt;a href="https://developer.hashicorp.com/terraform/language/functions/timestamp" rel="noopener noreferrer"&gt;timestamp&lt;/a&gt; and &lt;a href="https://developer.hashicorp.com/terraform/language/functions/timeadd" rel="noopener noreferrer"&gt;timeadd&lt;/a&gt; to generate a valid, formatted timestamp.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_virtual_desktop_host_pool_registration_info"&lt;/span&gt; &lt;span class="s2"&gt;"token"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;hostpool_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_virtual_desktop_host_pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
 &lt;span class="nx"&gt;expiration_date&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timeadd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s2"&gt;"2h"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_virtual_desktop_host_pool_registration_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The newly created token is passed to a local, local.token, that the vm_dsc_ext block will reference later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_virtual_machine_extension"&lt;/span&gt; &lt;span class="s2"&gt;"vm_dsc_ext"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nx"&gt;settings&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;SETTINGS&lt;/span&gt;&lt;span class="sh"&gt;
    {
      "modulesUrl": "https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/Configuration_09-08-2022.zip",
      "configurationFunction": "Configuration.ps1\\AddSessionHost",
      "properties": {
        "HostPoolName":"${azurerm_virtual_desktop_host_pool.pool.name}"
      }
    }
&lt;/span&gt;&lt;span class="no"&gt;SETTINGS
&lt;/span&gt;  &lt;span class="nx"&gt;protected_settings&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;PROTECTED_SETTINGS&lt;/span&gt;&lt;span class="sh"&gt;
  {
    "properties": {
      "registrationInfoToken": "${local.token}"
    }
  }
&lt;/span&gt;&lt;span class="no"&gt;PROTECTED_SETTINGS
&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Adding Workspace Support
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/state/workspaces" rel="noopener noreferrer"&gt;Terraform Workspaces&lt;/a&gt; are used to redeploy existing code by creating a new statefile.&lt;/p&gt;

&lt;p&gt;Terraform workspaces can be used to mirror one subscription's infrastructure into another. They can also programmatically enforce other changes - the production workspace and the development workspace may be configured to use a different default VM size.&lt;/p&gt;

&lt;p&gt;Resources created by the module should clearly indicate their associated workspace.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Resource names should indicate the workspace.&lt;/li&gt;
&lt;li&gt;The resource should be tagged with the workspace name.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;Using a series of conditional expressions and the &lt;a href="https://developer.hashicorp.com/terraform/language/functions/coalesce" rel="noopener noreferrer"&gt;coalesce&lt;/a&gt; function, Terraform will identify the workspace and select a three letter abbreviation to concat in the resource naming scheme.&lt;/p&gt;

&lt;p&gt;The module runs through the list of possible workspace names, checking each against a conditional expression.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the workspace name matches the conditional expression, the local is set to the conditional expression's true condition value (e.g. PRD).&lt;/li&gt;
&lt;li&gt;If the conditional expression resolves to false, the local is set to an empty string.
All the locals are fed to coalesce. The function returns the first value in the index that is not an empty string.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;production_workspace&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"PRD"&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
 &lt;span class="nx"&gt;development_workspace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"development"&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"DEV"&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
 &lt;span class="nx"&gt;uat_workspace&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"uat"&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"UAT"&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
 &lt;span class="nx"&gt;other_workspace&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"development"&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"uat"&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"TST"&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
 &lt;span class="nx"&gt;workspace_prefix&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;production_workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;development_workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uat_workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;other_workspace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module will also tag the resources with it's workspace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="nx"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"Production"&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Completed Module
&lt;/h2&gt;

&lt;p&gt;I now have a working module! If you'd like to see how all the code fits together, visit my &lt;a href="https://github.com/ColeHeard/terraform-azurerm-avd" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; repo.&lt;/p&gt;

&lt;p&gt;This post was focused on the module's code. Before I uploaded it to the &lt;a href="https://registry.terraform.io/modules/ColeHeard/avd/azurerm/latest" rel="noopener noreferrer"&gt;Terraform Registry&lt;/a&gt;, I performed other tasks that were outside the scope of this post. Highlights include automated documentation with Terraform-Docs, the inclusion of OOS licensing, and creating a release tag in GitHub.&lt;/p&gt;

&lt;p&gt;The code snippet below calls the module from the Terraform Registry. The example module is based on application pool described in Resources with Complicated Relationships.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="c1"&gt;# Required Input&lt;/span&gt;
 &lt;span class="nx"&gt;source&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ColeHeard/avd/azurerm"&lt;/span&gt;
 &lt;span class="nx"&gt;version&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;
 &lt;span class="nx"&gt;pool_type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt;
 &lt;span class="nx"&gt;rg&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main_rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
 &lt;span class="nx"&gt;region&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
 &lt;span class="nx"&gt;local_admin&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local_admin&lt;/span&gt;
 &lt;span class="nx"&gt;local_pass&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local_pass&lt;/span&gt;
 &lt;span class="nx"&gt;network_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azurerm_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;network&lt;/span&gt;
 &lt;span class="c1"&gt;# Optional Input&lt;/span&gt;
 &lt;span class="nx"&gt;application_map&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;red_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blue_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;yellow_app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="nx"&gt;managed_image_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azurerm_shared_image_version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;custom_image&lt;/span&gt;
 &lt;span class="nx"&gt;custom_rdp_properties&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rdp_app_streaming&lt;/span&gt;
 &lt;span class="nx"&gt;domain&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;
 &lt;span class="nx"&gt;domain_user&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain_user&lt;/span&gt;
 &lt;span class="nx"&gt;domain_pass&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain_pass&lt;/span&gt;
 &lt;span class="nx"&gt;workspace_id&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace_id&lt;/span&gt;
 &lt;span class="nx"&gt;workspace_key&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace_key&lt;/span&gt;
 &lt;span class="nx"&gt;vmsize&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard_D8as_v4"&lt;/span&gt;
 &lt;span class="nx"&gt;vmcount&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
 &lt;span class="nx"&gt;maximum_sessions_allowed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.reactiongifs.com%2Fwp-content%2Fuploads%2F2013%2F07%2Fralph-wave.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.reactiongifs.com%2Fwp-content%2Fuploads%2F2013%2F07%2Fralph-wave.gif" alt="Ralph Wiggum - Goodbye"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>azure</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
