<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Adam Connelly</title>
    <description>The latest articles on Forem by Adam Connelly (@adamconnelly).</description>
    <link>https://forem.com/adamconnelly</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%2F729407%2F0037b253-5380-4d22-8208-1730fabeacf9.png</url>
      <title>Forem: Adam Connelly</title>
      <link>https://forem.com/adamconnelly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/adamconnelly"/>
    <language>en</language>
    <item>
      <title>Spacelift Module Registry – What It is and How to Use It</title>
      <dc:creator>Adam Connelly</dc:creator>
      <pubDate>Wed, 01 Feb 2023 09:51:43 +0000</pubDate>
      <link>https://forem.com/spacelift/spacelift-module-registry-what-it-is-and-how-to-use-it-31ni</link>
      <guid>https://forem.com/spacelift/spacelift-module-registry-what-it-is-and-how-to-use-it-31ni</guid>
      <description>&lt;h2&gt;
  
  
  What is the Module Registry and Why Do I Care?
&lt;/h2&gt;

&lt;p&gt;The Spacelift module registry provides a private Terraform module registry that is fully compatible with Terraform. The module registry allows you to define reusable &lt;a href="https://spacelift.io/blog/what-are-terraform-modules-and-how-do-they-work" rel="noopener noreferrer"&gt;Terraform modules&lt;/a&gt; and share them with various different audiences. This includes sharing modules with multiple Stacks in your Spacelift account but also includes sharing modules with other accounts.&lt;/p&gt;

&lt;p&gt;The module registry is available to all Spacelift accounts regardless of tier, although certain functionality, like the ability to share modules with other Spacelift accounts, is only available on the Enterprise tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the Registry
&lt;/h2&gt;

&lt;p&gt;Using the module registry involves the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Declaring the module source.&lt;/li&gt;
&lt;li&gt;Adding the module to your Spacelift account.&lt;/li&gt;
&lt;li&gt;Deploying a version of that module.&lt;/li&gt;
&lt;li&gt;Consuming that version.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1. Declaring the module source
&lt;/h3&gt;

&lt;p&gt;Creating a module is simple, and if you already have an existing Terraform module, you’re almost ready to go. The only additional steps you need to take are defining a &lt;em&gt;config.yml&lt;/em&gt; file that describes the module as well as pointing Spacelift at your module source.&lt;/p&gt;

&lt;p&gt;The basic folder structure of a Spacelift module looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- /
 + - .spacelift/
   |
   + - config.yml
 + - examples/
   |
   + - example-1/
   + - example-2/
 + - main.tf
 + - ...
 + - something-else.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, as well as any Terraform definitions your module requires, there’s also a &lt;em&gt;.spacelift&lt;/em&gt; folder. This folder contains a single file: &lt;em&gt;config.yml&lt;/em&gt;. This config file defines certain pieces of information about your module, including its version, along with any test cases.&lt;/p&gt;

&lt;p&gt;A minimum example of a &lt;em&gt;config.yml&lt;/em&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# version defines the version of the config.yml configuration format. Currently the only option is 1.&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# module_version defines the version of the module to publish.&lt;/span&gt;
&lt;span class="na"&gt;module_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The folder structure above also includes an &lt;em&gt;examples&lt;/em&gt; folder. This folder is not required, and also does not need to be called &lt;em&gt;examples&lt;/em&gt;. It would typically contain one or more test cases that can double as usage examples for your module.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Adding the module to your Spacelift account
&lt;/h3&gt;

&lt;p&gt;There are two main ways to add a module to your Spacelift account: using the UI, or using the &lt;a href="https://spacelift.io/blog/terraform-providers" rel="noopener noreferrer"&gt;Terraform provider&lt;/a&gt;. The following steps will show how to add the module via the UI, but if you prefer to manage your account via Terraform, take a look at the &lt;a href="https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/module" rel="noopener noreferrer"&gt;spacelift_module&lt;/a&gt; resource for more information.&lt;/p&gt;

&lt;p&gt;The first step is to navigate to the modules section of the Spacelift UI and click the Add module button:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8kp2m39gbwmd4xixoh3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8kp2m39gbwmd4xixoh3.png" alt="add a module to spacelift registry" width="800" height="225"&gt;&lt;/a&gt;&lt;br&gt;
Next, choose the repository containing your module along with your main branch.&lt;/p&gt;

&lt;p&gt;For now, we’ll assume that your repo only contains a single module, so the project root can be left empty:&lt;/p&gt;

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

&lt;p&gt;For simple use-cases, you should be able to use the default behavior settings:&lt;/p&gt;

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

&lt;p&gt;Finally, fill out the description settings:&lt;/p&gt;

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

&lt;p&gt;For simple situations where your repository contains a single module and is named using &lt;a href="https://developer.hashicorp.com/terraform/registry/modules/publish#requirements" rel="noopener noreferrer"&gt;HashiCorp’s module repository naming scheme&lt;/a&gt; of &lt;code&gt;terraform-&amp;lt;PROVIDER&amp;gt;-&amp;lt;NAME&amp;gt;&lt;/code&gt;, Spacelift will automatically infer the Name and Provider for you, meaning that you don’t need to enter anything on this screen.&lt;/p&gt;

&lt;p&gt;For example, the repository used in the screenshots above was named &lt;code&gt;terraform-blog-8ball&lt;/code&gt;, giving it a name of &lt;em&gt;8ball&lt;/em&gt; and a provider of &lt;code&gt;blog&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Congratulations – you now have a module:&lt;/p&gt;

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

&lt;p&gt;Before you can actually use your module, you need to publish a version. To publish the initial version of your module, click on the &lt;em&gt;Trigger&lt;/em&gt; button. Once the version has been published, it should look something like this:&lt;/p&gt;

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

&lt;p&gt;Further versions can be published simply by pushing changes to your Git repository. The only thing to be aware of is that new versions will only be published if you update the &lt;code&gt;module_version&lt;/code&gt; in your &lt;em&gt;config.yml&lt;/em&gt; file. Another approach to module versioning using git tags is described later in this post.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Consuming that version
&lt;/h3&gt;

&lt;p&gt;OK great – we’ve got a version! Now what?&lt;/p&gt;

&lt;p&gt;Now that you have a version published, you need to consume that version from a stack. Consuming a Spacelift module is exactly the same as consuming a module from the Terraform registry. The only difference is you need to specify a different source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"8ball"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"spacelift.io/adamconnelly/8ball/blog"&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;"0.0.1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In fact, if you click the &lt;em&gt;Show Instructions&lt;/em&gt; button in the Spacelift UI, it will give you a usage example:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; in order to be able to access your module, Terraform needs to be authenticated with the Spacelift module registry. This happens automatically for you when running Terraform inside a Spacelift stack.&lt;/p&gt;

&lt;p&gt;If you want to be able to use a module from outside Spacelift, for example, if you want to test a module out locally, there are a few more steps involved. A few approaches are outlined later in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;One of the pieces of functionality that the Spacelift registry provides that isn’t part of the standard Terraform module registry is testing functionality. Each module can contain one or more test cases.&lt;/p&gt;

&lt;p&gt;Test cases serve two purposes: they provide you with confidence that any changes you make haven’t broken your module, and they serve as usage examples for any consumers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining a test case
&lt;/h3&gt;

&lt;p&gt;Each test case is like a mini stack that uses your module, making sure that it can be applied and then destroyed correctly. To add a test case, create a Terraform file that references your module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"basic_example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../../"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then reference that test case in your &lt;em&gt;config.yml&lt;/em&gt; file:&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;module_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.2&lt;/span&gt;

&lt;span class="na"&gt;tests&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;Basic Example&lt;/span&gt;
   &lt;span class="na"&gt;project_root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;examples/basic-example&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note, when referencing the module, you should use a relative reference (for example source = “../../”) and avoid specifying a version constraint. This ensures that your tests are run against the current version of your module code rather than a previously published version.&lt;/p&gt;

&lt;p&gt;Now that your test case has been defined commit your code, push and create a pull request. After opening your PR, you should be able to see it along with any test cases defined in the PRs tab for your module:&lt;/p&gt;

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

&lt;p&gt;If you click on a test case you can view the Spacelift run, showing the full plan, apply and destroy lifecycle:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; test cases will only be triggered if the version number of your module is increased. Please remember to do this, otherwise, your changes will be ignored!&lt;/p&gt;

&lt;h3&gt;
  
  
  Extending test cases via hooks
&lt;/h3&gt;

&lt;p&gt;Although the module registry gives you the ability to run one or more test cases, it doesn’t provide a particular test framework out of the box. However, you can extend your test cases using &lt;a href="https://docs.spacelift.io/concepts/stack/stack-settings.html#customizing-workflow" rel="noopener noreferrer"&gt;Workflow Customization&lt;/a&gt; (a.k.a. hooks) to run any additional checks you need.&lt;/p&gt;

&lt;p&gt;Let’s pretend that we want to extend the magic 8-ball module to allow a list of custom outcomes to be supplied. The first step would be to add a new variable to our module to support this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"custom_outcomes"&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;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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 set of custom outcomes that should be used by the 8-ball"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point we can define our new test case by extending our &lt;em&gt;config.yml&lt;/em&gt; file to look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;module_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.3&lt;/span&gt;

&lt;span class="na"&gt;tests&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;Basic example&lt;/span&gt;
   &lt;span class="na"&gt;project_root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;examples/basic-example&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;Custom outcomes&lt;/span&gt;
   &lt;span class="na"&gt;project_root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;examples/custom-outcomes&lt;/span&gt;
   &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;TF_VAR_custom_outcomes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;["definitely","likely","surely"]'&lt;/span&gt;
   &lt;span class="na"&gt;after_apply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="c1"&gt;# Get the value of our Terraform output&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ACTUAL_OUTCOME=$(terraform output -raw outcome)&lt;/span&gt;
     &lt;span class="c1"&gt;# Check it's in our set of custom outcomes&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "definitely likely surely" | grep "$ACTUAL_OUTCOME"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, a new test case has been defined named &lt;em&gt;Custom outcomes&lt;/em&gt;. It defines an environment variable &lt;code&gt;TF_VAR_custom_outcomes&lt;/code&gt; to allow us to define the set of outcomes via the config.yml file, and defines some &lt;code&gt;after_apply&lt;/code&gt; hooks that run the additional checks.&lt;/p&gt;

&lt;p&gt;In this simple example, all we want to do is get the value of a &lt;a href="https://spacelift.io/blog/terraform-output" rel="noopener noreferrer"&gt;Terraform output&lt;/a&gt; (&lt;code&gt;terraform output -raw outcome&lt;/code&gt;), and then check whether it exists in our list of expected outcomes (the echo and grep commands). The reason that this works is that an exit code of 0 from our custom hook will allow the test case to pass, whereas a non-zero exit code causes a failure.&lt;/p&gt;

&lt;p&gt;Now that our &lt;em&gt;config.yml&lt;/em&gt; file has been updated, it’s time to create the Terraform definition for the test case. It should look something like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Define an input variable that can be supplied via the test case definition&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"custom_outcomes"&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 set of custom outcomes that should be used by the 8-ball"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Invoke our module, and pass through the custom_outcomes variable&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"custom_outcomes_example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;source&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../../"&lt;/span&gt;
 &lt;span class="nx"&gt;custom_outcomes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;custom_outcomes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Define an output to pass the `outcome` output from our module out of our test case's root&lt;/span&gt;
&lt;span class="c1"&gt;# module so that we can assert on it via the `after_apply` hook&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"outcome"&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="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;custom_outcomes_example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outcome&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 try to run this test case now, it should result in a failure since we haven’t actually implemented the changes. You can see that in the following screenshot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr3qk9ufymqqs9b6wt24l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr3qk9ufymqqs9b6wt24l.png" alt="spacelift module registry custom outcomes" width="800" height="501"&gt;&lt;/a&gt;&lt;br&gt;
We could then add an implementation like the following to our module to get the test case to pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Allow a set of custom outcomes to be specified&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"custom_outcomes"&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;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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 set of custom outcomes that should be used by the 8-ball"&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;default_outcomes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"probably"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"maybe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"unlikely"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"outcome not looking good"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ask me again"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

 &lt;span class="c1"&gt;# Use the custom outcomes if they are provided, otherwise use the default outcomes&lt;/span&gt;
 &lt;span class="nx"&gt;outcomes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;custom_outcomes&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;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;custom_outcomes&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_outcomes&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"random_integer"&lt;/span&gt; &lt;span class="s2"&gt;"result"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
 &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outcomes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Output the result&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"outcome"&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="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outcomes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;random_integer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&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;
  
  
  Environment and Cloud Credentials
&lt;/h2&gt;

&lt;p&gt;Spacelift modules support &lt;a href="https://docs.spacelift.io/concepts/configuration/environment" rel="noopener noreferrer"&gt;environment variables&lt;/a&gt;, &lt;a href="https://docs.spacelift.io/concepts/configuration/context" rel="noopener noreferrer"&gt;context attachments&lt;/a&gt; and &lt;a href="https://docs.spacelift.io/integrations/cloud-providers/" rel="noopener noreferrer"&gt;Cloud credential integrations&lt;/a&gt;, just like a stack.&lt;/p&gt;

&lt;p&gt;The difference between module and stack environments is that with stacks, the environment is made available to each run, whereas with a module, the environment is made available to each test case that runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud Credentials
&lt;/h3&gt;

&lt;p&gt;Since each module test-case goes through a full plan-apply-destroy lifecycle, it’s important to understand that Terraform resources will be created by test case executions even though they will be destroyed as part of the test run. If your module creates resources in a cloud provider, we would recommend that you use a completely separate account dedicated for testing to avoid accidentally impacting other environments.&lt;/p&gt;

&lt;p&gt;On a related note, if you have multiple test-cases defined for your module, be aware that they could execute simultaneously, leading to conflicts with resource names. For example, if you created a module that contained the following S3 bucket definition, it could fail if two module tests attempted to create the resource at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"conflicting_bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"test-bucket"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, it would make more sense to use a &lt;code&gt;bucket_prefix&lt;/code&gt;, or to allow the bucket name to be parameterized via a &lt;a href="https://spacelift.io/blog/how-to-use-terraform-variables" rel="noopener noreferrer"&gt;Terraform variable&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"conflicting_bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;bucket_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"test-bucket"&lt;/span&gt;

 &lt;span class="c1"&gt;# or:&lt;/span&gt;
 &lt;span class="c1"&gt;# bucket = var.bucket_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Overriding test case environment
&lt;/h3&gt;

&lt;p&gt;The environment for test cases can be overridden on a case-by-case basis via the &lt;em&gt;config.yml&lt;/em&gt; file. The config file allows a set of default environment variables to be specified and then allows that environment to be overridden by each test:&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;module_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.1&lt;/span&gt;

&lt;span class="na"&gt;test_defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="na"&gt;TF_VAR_bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-bucket"&lt;/span&gt;

&lt;span class="na"&gt;tests&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;Test case &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
   &lt;span class="na"&gt;project_root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;examples/test-case-1&lt;/span&gt;

 &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test case &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
   &lt;span class="na"&gt;project_root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;examples/test-case-2&lt;/span&gt;
   &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;TF_VAR_bucket_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;test-bucket-2"&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;Test case &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
   &lt;span class="na"&gt;project_root&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;examples/test-case-3&lt;/span&gt;
   &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;TF_VAR_bucket_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;test-bucket-3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please note, Terraform variables don’t automatically propagate through to the underlying module used by each test case. This means that for the example above to work, each test case would need to contain Terraform code similar to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Declare a variable that will be populated by our test case config&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"bucket_name"&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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../../"&lt;/span&gt;

 &lt;span class="c1"&gt;# Reference that variable, and pass it through to our module&lt;/span&gt;
 &lt;span class="nx"&gt;bucket_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Integration with VCS Systems
&lt;/h2&gt;

&lt;p&gt;The module registry integrates with your VCS system to provide feedback while working on changes to modules in PRs, and also to connect published versions back to their commits. After publishing a new version, you should see a status against your commit that looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdnsovgf9g7scxmboz43b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdnsovgf9g7scxmboz43b.png" alt="spacelift module registry Integration with VCS Systems" width="800" height="293"&gt;&lt;/a&gt;&lt;br&gt;
Clicking on &lt;em&gt;details&lt;/em&gt; shows more information about the version that was published:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1vz2a0iqr68jio6qexe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1vz2a0iqr68jio6qexe.png" alt="spacelift module registry Integration with VCS Systems details" width="800" height="326"&gt;&lt;/a&gt;&lt;br&gt;
If your module has test cases associated with it, and you push a PR, the result of the checks will be displayed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftrsx98l3pmnge8211mer.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftrsx98l3pmnge8211mer.png" alt="spacelift module registry add simple example of using hooks for testing" width="800" height="715"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Storing Multiple Modules in a Single Repo
&lt;/h2&gt;

&lt;p&gt;Spacelift also supports using a single Git repo to store multiple modules. For example, you might have a structure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- /
 + - vpc/
   + - .spacelift/
     |
     + - config.yml
   + - main.tf
 + - eks/
 + - sqs/
 + - s3/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, we’re defining four modules: vpc; eks; sqs; and s3. The contents of the eks, sqs and s3 modules have been omitted for simplicity, but each would also contain a &lt;em&gt;config.yml&lt;/em&gt; file along with any required Terraform definitions.&lt;/p&gt;

&lt;p&gt;To support this, simply set the project root of each module. Here’s what the &lt;em&gt;vpc&lt;/em&gt; module’s settings might look like:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Versioning
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Manually via the config.yml file
&lt;/h3&gt;

&lt;p&gt;The simplest way to version your modules is to manually update the &lt;code&gt;module_version&lt;/code&gt; property in your &lt;em&gt;config.yml&lt;/em&gt; file:&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# Update module_version before publishing a new version&lt;/span&gt;
&lt;span class="na"&gt;module_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typical workflow might look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make any required changes to your module.&lt;/li&gt;
&lt;li&gt;Edit config.yml and increase module_version as required.&lt;/li&gt;
&lt;li&gt;Create a pull request and make sure any tests pass.&lt;/li&gt;
&lt;li&gt;Once your PR has been approved, merge it to publish the new version.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Automatically via tags
&lt;/h3&gt;

&lt;p&gt;If you don’t want to have to manually edit your &lt;em&gt;config.yml&lt;/em&gt; file each time you make a change, another option is to use a tag based approach. To do this, create a new push policy with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package spacelift

module_version := version {
   version := trim_prefix(input.push.tag, "v")
}

propose { true }
track { input.push.tag != "" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This policy tells Spacelift to trigger version publishing whenever a new tag in the format &lt;code&gt;vx.y.z&lt;/code&gt; is pushed to your repo and also tells Spacelift to use the tag as the version number. Because Terraform module versions don’t support the &lt;code&gt;v&lt;/code&gt; prefix, the policy removes that from the start of the tag.&lt;/p&gt;

&lt;p&gt;To enable the policy, attach it to your module:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb8ag6op8fpa37kx0c44a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb8ag6op8fpa37kx0c44a.png" alt="spacelift module registry policy" width="800" height="776"&gt;&lt;/a&gt;&lt;br&gt;
Now that your policy is in place, pushes to your main branch will not trigger new versions to be published. Instead a new version will be published when a new tag is pushed to your repository.&lt;/p&gt;
&lt;h2&gt;
  
  
  Automatically Triggering Consumers
&lt;/h2&gt;

&lt;p&gt;Spacelift keeps track of the stacks that consume particular modules. This means that you can automatically trigger any consumers when a new version of the module is published. To do this, you need to do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make sure that any Terraform version constraints allow new versions to be used.&lt;/li&gt;
&lt;li&gt;Attach a trigger policy to your module telling it to trigger any consumers.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Version constraints
&lt;/h3&gt;

&lt;p&gt;When referencing a Terraform module, you can specify an optional version constraint. This defines the set of versions that your stack is able to use. For example, if you published version 2.0.0 of a module, but had the following module reference, the new version would not be picked up automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"version_example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"spacelift.io/adamconnelly/8ball/blog"&lt;/span&gt;
 &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.0.0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Trigger policy
&lt;/h3&gt;

&lt;p&gt;Trigger policies for modules accept the list of consumers of the current latest version of the module as an input to the policy. This allows you to write a simple trigger policy like the following to automatically re-trigger any consumers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package spacelift

trigger[stack.id] {
 stack := input.stacks[_]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have attached this trigger policy to your module and released a new version of the module, you should see runs triggered on any stacks that are currently using the latest version of the module. It should look something like this:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Please be aware that the list of stacks provided to the trigger policy only includes those that are using the _latest _ version of the module at the time that the new version is triggered. This means that if the latest version of a module was 0.0.5, but a stack is currently only using version 0.0.4 of your module, it will not automatically be included in the list of consumers. To fix this, you will need to manually trigger your stack once to make sure it’s using the latest version.&lt;/p&gt;

&lt;p&gt;For more information, see our &lt;a href="https://docs.spacelift.io/concepts/policy/trigger-policy#module-updates" rel="noopener noreferrer"&gt;trigger policy&lt;/a&gt; documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing Modules with Other Accounts
&lt;/h2&gt;

&lt;p&gt;Spacelift modules can optionally be used by other Spacelift accounts than the one the module is defined in. This can be useful, for example, if you have multiple Spacelift accounts or if you have clients who also use Spacelift who use your custom Terraform modules.&lt;/p&gt;

&lt;p&gt;The list of accounts that can access a module can be configured via the &lt;em&gt;sharing&lt;/em&gt; section of the module settings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flvncpwwl91mepnlr3za7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flvncpwwl91mepnlr3za7.png" alt="Sharing Modules with Other Accounts" width="800" height="650"&gt;&lt;/a&gt;&lt;br&gt;
For more information, see our &lt;a href="https://docs.spacelift.io/vendors/terraform/module-registry#sharing-modules" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. Also, please note that module sharing is only available to customers on the Enterprise tier.&lt;/p&gt;
&lt;h2&gt;
  
  
  Using Modules Outside of Spacelift
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Using modules locally
&lt;/h3&gt;

&lt;p&gt;If you want to use Spacelift modules while running Terraform locally, you need to tell Terraform how to authenticate with the Spacelift registry. The easiest way to do this is via the terraform login command. To do this, run the following command and then follow the instructions to login to your Spacelift account via your web browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform login spacelift.io
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once authentication has completed successfully, you should see a message similar to the following in your console:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frs24iz5qlg7te00erp2f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frs24iz5qlg7te00erp2f.png" alt="spacelift module registry using modules locally" width="800" height="111"&gt;&lt;/a&gt;&lt;br&gt;
Now that you are authenticated, you can add a reference to a Spacelift module and run terraform commands locally as normal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using modules in an automated environment
&lt;/h3&gt;

&lt;p&gt;If you need to use a module in an automated environment outside of Spacelift, the best bet is to use an API key. Please see our &lt;a href="https://docs.spacelift.io/integrations/api#spacelift-api-key-token" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; for more information on generating and using API keys.&lt;/p&gt;

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

&lt;p&gt;For more information about the Spacelift module registry, please see the documentation available at &lt;a href="https://docs.spacelift.io/vendors/terraform/module-registry" rel="noopener noreferrer"&gt;docs.spacelift.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;💡 You might also like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/5-ways-to-manage-terraform-at-scale" rel="noopener noreferrer"&gt;5 Ways to Manage Terraform at Scale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/terraform-automation" rel="noopener noreferrer"&gt;How to Automate Terraform Deployments and Infrastructure Provisioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/how-specialized-solution-can-improve-your-iac" rel="noopener noreferrer"&gt;How to Improve Your Infrastructure as Code using Terraform&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Monitoring Your Spacelift Account via Prometheus</title>
      <dc:creator>Adam Connelly</dc:creator>
      <pubDate>Wed, 17 Aug 2022 11:53:47 +0000</pubDate>
      <link>https://forem.com/spacelift/monitoring-your-spacelift-account-via-prometheus-1koo</link>
      <guid>https://forem.com/spacelift/monitoring-your-spacelift-account-via-prometheus-1koo</guid>
      <description>&lt;p&gt;For a while now, we have received feedback from customers that they would like to be able to connect their Spacelift account to external monitoring systems. Today we are excited to announce the &lt;a href="https://github.com/spacelift-io/prometheus-exporter"&gt;Prometheus Exporter for Spacelift&lt;/a&gt;! We also have plans to add support for &lt;em&gt;&lt;/em&gt;, so if you’re using another system, we should have you covered soon.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Can It Do?
&lt;/h2&gt;

&lt;p&gt;The Prometheus exporter allows you to monitor various metrics about your Spacelift account over time. You can then use tools like Grafana to visualize those changes and Alertmanager to take actions based on account metrics. Several metrics are available, and you can find the complete list of available metrics &lt;a href="https://github.com/spacelift-io/prometheus-exporter#available-metrics"&gt;here&lt;/a&gt;. Below are a few examples of the information the exporter currently provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The number of runs pending and currently executing in both public and private worker pools.&lt;/li&gt;
&lt;li&gt;The number of workers in a pool.&lt;/li&gt;
&lt;li&gt;Usage information, including the number of public and private worker minutes used during the current billing period.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you have that information, it opens a number of possibilities, including visualizing information about your account via Grafana dashboards, alerting on events like a lack of private workers, as well as using that information to autoscale worker pools via the Horizontal Pod Autoscaler.&lt;/p&gt;

&lt;p&gt;To give you a taste, here’s an example Grafana dashboard showing some of the information available from the exporter:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5GCI59gR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kn2jzj5z22aocy7vumea.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5GCI59gR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kn2jzj5z22aocy7vumea.png" alt="Image description" width="880" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Does It Work?
&lt;/h2&gt;

&lt;p&gt;The Prometheus exporter is an adaptor between Prometheus and the Spacelift GraphQL API. Whenever Prometheus asks for the current metrics, the exporter makes a GraphQL request and converts it into the metrics format Prometheus expects.&lt;/p&gt;

&lt;p&gt;The following diagram gives an overview of this process:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7vdfClWI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4n27hx8ik3cr1kvuglxz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7vdfClWI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4n27hx8ik3cr1kvuglxz.png" alt="Image description" width="880" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Ok, great, but how do I use the Prometheus Exporter?&lt;/p&gt;

&lt;p&gt;To help with setup and deployment, we are going to guide you through the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy a Prometheus stack to a Kubernetes cluster.&lt;/li&gt;
&lt;li&gt;Deploy the exporter.&lt;/li&gt;
&lt;li&gt;Configure  Prometheus to monitor it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://github.com/spacelift-io/prometheus-exporter#quick-start"&gt;Quick Start&lt;/a&gt; section in the exporter repo outlines several options for deploying the exporter. Review the best deployment option that makes sense, depending on your setup and requirements.&lt;/p&gt;

&lt;p&gt;If you already have a Prometheus stack setup and have plenty of experience, feel free to skip to the “Installing the Prometheus Exporter” section.&lt;/p&gt;

&lt;p&gt;We also assume that you already have a Kubernetes cluster provisioned (hopefully &lt;a href="https://docs.spacelift.io/vendors/kubernetes"&gt;via Spacelift!&lt;/a&gt;) and available to complete the following steps. If not, a local installation like &lt;a href="https://minikube.sigs.k8s.io/docs/start/"&gt;Minikube&lt;/a&gt; will work fine for illustration purposes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing kube-prometheus-stack
&lt;/h3&gt;

&lt;p&gt;The first step is to install the &lt;a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack"&gt;kube-prometheus-stack Helm chart&lt;/a&gt;. You can do that with the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install --create-namespace -n monitoring kube-prometheus prometheus-community/kube-prometheus-stack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That will install a Prometheus, Grafana, and Alertmanager stack into your cluster’s namespace called &lt;code&gt;monitoring&lt;/code&gt;. Run the &lt;code&gt;kubectl get pods -n monitoring&lt;/code&gt; command to see the installed components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get pods -n monitoring
NAME                                                     READY   STATUS    RESTARTS       AGE
alertmanager-kube-prometheus-kube-prome-alertmanager-0   2/2     Running   14 (58m ago)   20d
kube-prometheus-grafana-5cd6d47467-2rt88                 3/3     Running   21 (58m ago)   20d
kube-prometheus-kube-prome-operator-54b7488f58-7fmfv     1/1     Running   7 (58m ago)    20d
kube-prometheus-kube-state-metrics-8ccff67b4-zlfx9       1/1     Running   10 (58m ago)   20d
kube-prometheus-prometheus-node-exporter-zv8zn           1/1     Running   7 (58m ago)    20d
prometheus-kube-prometheus-kube-prome-prometheus-0       2/2     Running   14 (58m ago)   20d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Getting API Credentials
&lt;/h3&gt;

&lt;p&gt;The Prometheus exporter authenticates to the Spacelift GraphQL API using an API key. Follow the &lt;a href="https://docs.spacelift.io/integrations/api#spacelift-api-key-greater-than-token"&gt;guide to create a new API key&lt;/a&gt; required by the explorer. After you create your key, please note the API Key ID and API Key Secret – you’ll need both when configuring the exporter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing the Prometheus Exporter
&lt;/h3&gt;

&lt;p&gt;The exporter is available via a Docker image published to the &lt;code&gt;public.ecr.aws/spacelift/promex&lt;/code&gt; container registry. To deploy the exporter to Kubernetes, we need to create the following resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Deployment – to run the exporter container.&lt;/li&gt;
&lt;li&gt;A Service – to allow Prometheus to scrape the exporter.&lt;/li&gt;
&lt;li&gt;A ServiceMonitor – to let Prometheus know that it needs to  scrape the exporter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following is an example Deployment definition for running the exporter. Make sure to replace the &lt;code&gt;&amp;lt;account name&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;API Key&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;API Secret&amp;gt;&lt;/code&gt; placeholders with the correct values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
 name: spacelift-promex
 labels:
   app: spacelift-promex
spec:
 replicas: 1
 selector:
   matchLabels:
     app: spacelift-promex
 template:
   metadata:
     labels:
       app: spacelift-promex
   spec:
     containers:
     - name: spacelift-promex
       image: public.ecr.aws/spacelift/promex:latest
       ports:
       - name: metrics
         containerPort: 9953
       readinessProbe:
         httpGet:
           path: /health
           port: metrics
         periodSeconds: 5
       env:
       - name: "SPACELIFT_PROMEX_API_ENDPOINT"
         value: "https://&amp;lt;account name&amp;gt;.app.spacelift.io"
       - name: "SPACELIFT_PROMEX_API_KEY_ID"
         value: "&amp;lt;API Key&amp;gt;"
       - name: "SPACELIFT_PROMEX_API_KEY_SECRET"
         value: "&amp;lt;API Secret&amp;gt;"
       - name: "SPACELIFT_PROMEX_LISTEN_ADDRESS"
         value: ":9953"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The example above defines the API key and secret as normal environment variables. We would recommend that you use Kubernetes Secrets for anything other than testing purposes.&lt;/p&gt;

&lt;p&gt;Next, create a Service to expose the exporter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
 name: spacelift-promex
 labels:
   app: spacelift-promex
spec:
 selector:
   app: spacelift-promex
 ports:
   - name: http-metrics
     protocol: TCP
     port: 80
     targetPort: metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally create your ServiceMonitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
 name: app-monitor
 labels:
   app: app-monitor
   release: kube-prometheus
spec:
 jobLabel: app-monitor
 selector:
   matchExpressions:
   - {key: app, operator: Exists}
 namespaceSelector:
   matchNames:
   - monitoring
 endpoints:
 - port: http-metrics
   interval: 15s
   path: "/metrics"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ServiceMonitor definition above tells Kubernetes to scrape any services that contain an &lt;code&gt;app&lt;/code&gt; label. The granularity of the metrics can be increased or decreased depending on requirements and organizational standards. The above example is configured for 15-second intervals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Viewing your Metrics
&lt;/h3&gt;

&lt;p&gt;Once you have your Prometheus stack up and running and have deployed the Spacelift exporter, you can use port-forwarding to access each component. First, let’s port-forward the exporter to port 8080 locally using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl port-forward service/spacelift-promex -n monitoring 8080:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assuming all is well, you should be able to see the raw metrics output by accessing &lt;a href="http://localhost:8080/metrics:"&gt;http://localhost:8080/metrics:&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--61Rl5Oo1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mzqt8qgh9r5t87fm8k2t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--61Rl5Oo1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mzqt8qgh9r5t87fm8k2t.png" alt="Image description" width="751" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also port-forward to your Grafana instance to view the metrics in Grafana:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl port-forward service/kube-prometheus-grafana -n monitoring 8081:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then quickly discover the available metrics via the Grafana Explore view:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mx2ZuWRT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o6b6e4hc6i98fyw9ftbq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mx2ZuWRT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o6b6e4hc6i98fyw9ftbq.png" alt="Image description" width="880" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Alerting
&lt;/h2&gt;

&lt;p&gt;One of the things that I think is amazing about the Prometheus stack is the ability to use PromQL queries not just for monitoring but for defining alerts. For example, if we want to trigger an alert whenever our worker pool has no available workers for a specific time period, we can use a query like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max(spacelift_worker_pool_workers) by (worker_pool_id, worker_pool_name) &amp;lt;= 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, if we want to alert when the number of queued runs for a pool gets too high, we can use a query like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max(spacelift_worker_pool_runs_pending) by (worker_pool_id, worker_pool_name) &amp;gt;= 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Autoscaling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; This section is intended to outline some of the possibilities that exist when using the Prometheus exporter and should NOT be used as a guide for a production-ready autoscaling solution. For example, it does not consider properly draining workers before scaling them down to avoid in-progress runs being terminated.&lt;/p&gt;

&lt;p&gt;Since we have metrics related to queued runs, we can use them to autoscale private workers. We can use the &lt;code&gt;spacelift_worker_pool_runs_pending&lt;/code&gt; metric, which tells us how many runs for a given worker pool are waiting to be scheduled, to detect when we need to add more workers to our pool. Similarly, we can use the &lt;code&gt;spacelift_worker_pool_workers&lt;/code&gt; and &lt;code&gt;spacelift_worker_pool_workers_busy&lt;/code&gt; metrics to decide when to scale down.&lt;/p&gt;

&lt;p&gt;We need to take both sets of metrics into account to avoid scaling down just because there are no queued runs. In this situation, we might still have runs in progress that need workers.&lt;/p&gt;

&lt;p&gt;For this to work, we need the following components available:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A working Prometheus stack with the Spacelift Prometheus Exporter running.&lt;/li&gt;
&lt;li&gt;A Spacelift &lt;a href="https://github.com/spacelift-io/spacelift-workerpool-k8s"&gt;worker pool&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://github.com/kubernetes-sigs/prometheus-adapter"&gt;prometheus-adapter&lt;/a&gt; installation.&lt;/li&gt;
&lt;li&gt;A Horizontal Pod Autoscaler resource to tell Kubernetes how to scale our worker pool.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the sake of simplicity, I’m going to assume that you already have steps 1 and 2 covered, and let’s assume that you’ve deployed the Spacelift worker pool chart with the default settings. I’ll also assume that you have a standard installation of the &lt;a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack"&gt;kube-prometheus-stack&lt;/a&gt; chart.&lt;/p&gt;

&lt;p&gt;The first thing we need to do is set up a Prometheus-adapter installation. The Prometheus-adapter works like a bridge between the Kubernetes metrics API and your Prometheus installation. It allows you to use Prometheus metrics to make autoscaling decisions within your cluster. We can visualize it something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FtoPeTX4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/adttt95r37sb5f90dqs5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FtoPeTX4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/adttt95r37sb5f90dqs5.png" alt="Image description" width="880" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Prometheus-adapter uses a configuration format to map between Prometheus queries and Kubernetes metrics. In our case, we can use something like the following to generate the two metrics we need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prometheus:
 # The URL points at the Kubernetes Service for Prometheus
 url: "http://kube-prometheus-kube-prome-prometheus"

rules:
 default: false

 external:
 # Define the spacelift_worker_pool_runs_pending metric
 - seriesQuery: '{__name__=~"^spacelift_worker_pool_runs_pending$"}'
   resources:
     template: &amp;lt;&amp;lt;.Resource&amp;gt;&amp;gt;
   name:
     as: "spacelift_worker_pool_runs_pending"
   metricsQuery: max(spacelift_worker_pool_runs_pending) by (worker_pool_id, worker_pool_name)
 # Define the spacelift_worker_pool_utilization metric
 - seriesQuery: '{__name__=~"^spacelift_worker_pool_workers$"}'
   resources:
     template: &amp;lt;&amp;lt;.Resource&amp;gt;&amp;gt;
   name:
     as: "spacelift_worker_pool_utilization"
   metricsQuery: |
     (max(spacelift_worker_pool_workers_busy) by (worker_pool_id, worker_pool_name)
       / max(spacelift_worker_pool_workers) by (worker_pool_id, worker_pool_name))
     or vector(0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deploying the adapter, you can query your metrics via the Kubernetes API, using the following commands (assuming you’ve deployed everything to a namespace called &lt;code&gt;monitoring&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get --raw /apis/external.metrics.k8s.io/v1beta1/namespaces/monitoring/spacelift_worker_pool_runs_pending
kubectl get --raw /apis/external.metrics.k8s.io/v1beta1/namespaces/monitoring/spacelift_worker_pool_utilization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we can create a Horizontal Pod Autoscaler definition to scale our worker pool Deployment based on those metrics automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
 name: spacelift-worker-hpa
spec:
 scaleTargetRef:
   apiVersion: apps/v1
   kind: Deployment
   name: spacelift-worker
 minReplicas: 1
 maxReplicas: 15
 metrics:
 - type: External
   external:
     metric:
       name: spacelift_worker_pool_runs_pending
       selector:
         matchLabels:
           worker_pool_id: "01G8Y06VGHCT17453VEE9T4YBZ"
     target:
       type: AverageValue
       averageValue: 1
 - type: External
   external:
     metric:
       name: spacelift_worker_pool_utilization
       selector:
         matchLabels:
           worker_pool_id: "01G8Y06VGHCT17453VEE9T4YBZ"
     target:
       type: Value
       value: 0.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the metrics are grouped by the &lt;code&gt;worker_pool_id&lt;/code&gt;; we can use this to target an individual worker pool when creating our HPA!&lt;/p&gt;

&lt;p&gt;That’s it – bask in your autoscaling glory:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GNsJfaYo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/128drzg7zudndgdwjree.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GNsJfaYo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/128drzg7zudndgdwjree.png" alt="Image description" width="880" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are excited to learn how our customers use the exporter and the problems they solve with it! Feel free to provide feedback and comments via Issues in the &lt;a href="https://github.com/spacelift-io/prometheus-exporter"&gt;Prometheus Exporter Github repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>prometheus</category>
    </item>
    <item>
      <title>Running Spacelift CI/CD workers in Kubernetes using DinD</title>
      <dc:creator>Adam Connelly</dc:creator>
      <pubDate>Mon, 07 Feb 2022 14:20:03 +0000</pubDate>
      <link>https://forem.com/spacelift/running-spacelift-cicd-workers-in-kubernetes-using-dind-30g0</link>
      <guid>https://forem.com/spacelift/running-spacelift-cicd-workers-in-kubernetes-using-dind-30g0</guid>
      <description>&lt;p&gt;For a while now at &lt;a href="https://spacelift.io/" rel="noopener noreferrer"&gt;Spacelift&lt;/a&gt; we’ve been hearing from users that they’d like to be able to run &lt;a href="https://docs.spacelift.io/concepts/worker-pools" rel="noopener noreferrer"&gt;Spacelift worker pools&lt;/a&gt; in their Kubernetes clusters. As a first step towards that we decided to investigate whether we could run workers in Kubernetes using a Docker-in-Docker sidecar container. The rest of this post gives a rough overview of the architecture of Spacelift workers, and also explains how the sidecar container strategy works.&lt;/p&gt;

&lt;p&gt;I’d just like to say thanks to &lt;a href="https://medium.com/@v.m_hs" rel="noopener noreferrer"&gt;Vadym Martsynovskyy&lt;/a&gt; for his great article about running &lt;a href="https://medium.com/hootsuite-engineering/building-docker-images-inside-kubernetes-42c6af855f25" rel="noopener noreferrer"&gt;Jenkins agents in Kubernetes&lt;/a&gt;. In that article he outlines the general approach taken here, and also describes some of the pros and cons of using Docker-in-Docker. It’s definitely well worth a read.&lt;/p&gt;

&lt;h1&gt;
  
  
  Background
&lt;/h1&gt;

&lt;p&gt;Before going into more detail, it’s worth explaining what a Spacelift worker is, and giving a quick overview of our architecture. At Spacelift, we provide a hybrid-SaaS model like many other CI/CD systems. What this means in practice is that the control plane that handles scheduling runs is managed by us, but the execution of runs happens in a separate process called a worker. We provide a public worker pool, but also allow you to self-host your own workers, providing complete control over your infrastructure.&lt;/p&gt;

&lt;p&gt;Perhaps confusingly, if you follow the instructions to create a private worker pool, you’ll notice that it involves downloading something called the “launcher”. The launcher is responsible for communicating with the Spacelift mothership (control plane), and creating and destroying worker containers using Docker for each Spacelift run:&lt;/p&gt;

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

&lt;p&gt;As you can see, the launcher process needs access to a Docker daemon in order to create a container for each run.&lt;/p&gt;

&lt;h1&gt;
  
  
  Running in Kubernetes
&lt;/h1&gt;

&lt;p&gt;We found that the launcher architecture maps very nicely to Kubernetes using a sidecar container to run the Docker daemon. For each worker in the pool, we need to create a Kubernetes Pod with two containers: one for the launcher; and another for Docker. The following shows a stripped down Deployment definition for creating a worker pool with 4 workers:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;worker-pool-1-spacelift-worker&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
 &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;containers&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;launcher&lt;/span&gt;
         &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public.ecr.aws/spacelift/launcher:latest"&lt;/span&gt;
         &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Always&lt;/span&gt;
         &lt;span class="na"&gt;env&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;DOCKER_HOST&lt;/span&gt;
             &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tcp://localhost:2375&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;SPACELIFT_TOKEN&lt;/span&gt;
             &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;..."&lt;/span&gt;
           &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SPACELIFT_POOL_PRIVATE_KEY&lt;/span&gt;
             &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;..."&lt;/span&gt;
         &lt;span class="na"&gt;volumeMounts&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;launcher-storage&lt;/span&gt;
             &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/opt/spacelift&lt;/span&gt;
             &lt;span class="na"&gt;subPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spacelift&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;dind&lt;/span&gt;
         &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker:dind"&lt;/span&gt;
         &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Always&lt;/span&gt;
         &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dockerd"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--host"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tcp://127.0.0.1:2375"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
         &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
           &lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
         &lt;span class="na"&gt;volumeMounts&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;launcher-storage&lt;/span&gt;
             &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/docker&lt;/span&gt;
             &lt;span class="na"&gt;subPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&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;launcher-storage&lt;/span&gt;
             &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/opt/spacelift&lt;/span&gt;
             &lt;span class="na"&gt;subPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;spacelift&lt;/span&gt;
     &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;launcher-storage&lt;/span&gt;
         &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The launcher communicates with the Docker-in-Docker sidecar container via TCP, which is configured via the &lt;code&gt;DOCKER_HOST&lt;/code&gt; environment variable for the launcher container. This works because the containers in a Kubernetes Pod share the same &lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/#pod-networking" rel="noopener noreferrer"&gt;IP address and port space, and can communicate with each other via localhost&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In addition, a shared volume called &lt;code&gt;launcher-storage&lt;/code&gt; is mounted into each container. This is used by the launcher to store the workspaces for runs, along with other things like cached tool binaries (for example Terraform). The Docker sidecar needs access to the run workspaces in order to mount them into the worker containers, and it also uses that volume to store its image cache.&lt;/p&gt;

&lt;p&gt;The last thing worth pointing out is that the &lt;code&gt;dind&lt;/code&gt; container sets &lt;code&gt;securityContext.privileged&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;. This is required for Docker-in-Docker to function correctly.&lt;/p&gt;

&lt;h1&gt;
  
  
  Launcher Image
&lt;/h1&gt;

&lt;p&gt;The Spacelift launcher is distributed as a statically linked binary, making it simple to build an image. As you can see, the Dockerfile simply copies the launcher into an Alpine container, and then sets the startup command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; alpine:3.14&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; spacelift-launcher /usr/bin/spacelift-launcher&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;755 /usr/bin/spacelift-launcher
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; [ "/usr/bin/spacelift-launcher" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This image is rebuilt and published to our public ECR repository any time the launcher binary is updated.&lt;/p&gt;

&lt;h1&gt;
  
  
  Helm Chart
&lt;/h1&gt;

&lt;p&gt;We provide &lt;a href="https://spacelift.io/blog/what-are-terraform-modules-and-how-do-they-work" rel="noopener noreferrer"&gt;Terraform modules&lt;/a&gt; to make it really easy for users to deploy worker pools to AWS, Azure and GCP, and we wanted to provide a similar experience for Kubernetes. To achieve this, we created a Helm chart that makes it simple to deploy workers to Kubernetes clusters: &lt;a href="https://github.com/spacelift-io/spacelift-workerpool-k8s" rel="noopener noreferrer"&gt;https://github.com/spacelift-io/spacelift-workerpool-k8s&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Assuming you’ve already configured your worker pool in Spacelift and have access to the credentials for the pool, deploying this chart to your cluster simply requires two steps.&lt;/p&gt;

&lt;p&gt;First, add the Spacelift Helm chart repository and update your local chart cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add spacelift https://downloads.spacelift.io/helm
helm repo update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, install the chart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade worker-pool-1 spacelift/spacelift-worker &lt;span class="nt"&gt;--install&lt;/span&gt; &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="s2"&gt;"credentials.token=&amp;lt;worker-pool-token&amp;gt;,credentials.privateKey=&amp;lt;worker-pool-private-key&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;worker-pool-token&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;worker-pool-private-key&amp;gt;&lt;/code&gt; with your own credentials, and make sure to base64-encode the private key.&lt;/p&gt;

&lt;p&gt;If all goes well, you should be able to view the pods for your worker pool using &lt;code&gt;kubectl get pods&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods
NAME                                              READY   STATUS    RESTARTS   AGE
worker-pool-1-spacelift-worker-7fcfc9f594-f94tj   2/2     Running   0          22m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should also be able to view the workers in your pool in Spacelift:&lt;/p&gt;

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

&lt;p&gt;When implementing in a production environment, you may want to use an alternative approach to providing credentials, and may want to configure a custom storage volume for the worker Pods. For more information about configuring the chart, check out the README in the chart GitHub repo.&lt;/p&gt;

&lt;h1&gt;
  
  
  Closing Thoughts
&lt;/h1&gt;

&lt;p&gt;If you’re interested in running Spacelift workers in Kubernetes, we’d welcome any feedback about the approach, contributions to the Helm chart, and also any issues you encounter so that we can make improvements. Also, if you aren’t already using Spacelift but are interested in trying it, you can signup and start your free evaluation of Spacelift &lt;a href="https://spacelift.io/free-trial" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;(The original post was published at &lt;a href="https://spacelift.io/blog/ci-cd-workers-in-kubernetes-dind" rel="noopener noreferrer"&gt;Spacelift&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>kubernetes</category>
      <category>docker</category>
    </item>
    <item>
      <title>Managing Azure Resources using Spacelift</title>
      <dc:creator>Adam Connelly</dc:creator>
      <pubDate>Thu, 18 Nov 2021 23:37:00 +0000</pubDate>
      <link>https://forem.com/spacelift/managing-azure-resources-using-spacelift-36oh</link>
      <guid>https://forem.com/spacelift/managing-azure-resources-using-spacelift-36oh</guid>
      <description>&lt;p&gt;In this post I want to show how easy it is to create a &lt;a href="https://spacelift.io/" rel="noopener noreferrer"&gt;Spacelift&lt;/a&gt; Stack that manages Azure resources. To keep things simple, I’m going to assume that you already know what Spacelift can do, and the basics of creating and running Stacks, but if not check out our &lt;a href="https://github.com/spacelift-io/terraform-starter" rel="noopener noreferrer"&gt;terraform-starter&lt;/a&gt; repo that walks you through the process of setting up an account, and getting started.&lt;/p&gt;

&lt;h1&gt;
  
  
  Overview
&lt;/h1&gt;

&lt;p&gt;In order to allow Spacelift to manage Azure resources, we need to do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a Service Principal in Azure with the correct ARM permissions to manage our resources.&lt;/li&gt;
&lt;li&gt;Create a Stack in Spacelift that uses the &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs" rel="noopener noreferrer"&gt;Azure&lt;/a&gt; provider.&lt;/li&gt;
&lt;li&gt;Give our Stack the credentials for our Service Principal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of the information you need to configure the Azure provider can be found &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure" rel="noopener noreferrer"&gt;here&lt;/a&gt;. This post just explains how to combine that information with Spacelift.&lt;/p&gt;

&lt;h1&gt;
  
  
  Creating our Service Principal
&lt;/h1&gt;

&lt;p&gt;To create our Service Principal, we need to use the &lt;code&gt;az ad sp create-for-rbac&lt;/code&gt; command (replacing with your actual subscription ID):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;az&lt;/span&gt; &lt;span class="nx"&gt;ad&lt;/span&gt; &lt;span class="nx"&gt;sp&lt;/span&gt; &lt;span class="nx"&gt;create-for-rbac&lt;/span&gt; &lt;span class="nx"&gt;--name&lt;/span&gt; &lt;span class="nx"&gt;spacelift-sp&lt;/span&gt; &lt;span class="nx"&gt;--role&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Contributor"&lt;/span&gt; &lt;span class="nx"&gt;--scopes&lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/subscriptions/&amp;lt;subscription-id&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will output something like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;Changing&lt;/span&gt; &lt;span class="s2"&gt;"spacelift-sp"&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt; &lt;span class="nx"&gt;URI&lt;/span&gt; &lt;span class="nx"&gt;of&lt;/span&gt; &lt;span class="s2"&gt;"http://spacelift-sp"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;which&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="nx"&gt;used&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="nx"&gt;principal&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;
&lt;span class="nx"&gt;Creating&lt;/span&gt; &lt;span class="s1"&gt;'Contributor'&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="nx"&gt;assignment&lt;/span&gt; &lt;span class="nx"&gt;under&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="s1"&gt;'/subscriptions/&amp;lt;subscription-id&amp;gt;'&lt;/span&gt;
&lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="nx"&gt;includes&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;you&lt;/span&gt; &lt;span class="nx"&gt;must&lt;/span&gt; &lt;span class="nx"&gt;protect&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Be&lt;/span&gt; &lt;span class="nx"&gt;sure&lt;/span&gt; &lt;span class="nx"&gt;that&lt;/span&gt; &lt;span class="nx"&gt;you&lt;/span&gt; &lt;span class="nx"&gt;do&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="nx"&gt;these&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;your&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="nx"&gt;or&lt;/span&gt; &lt;span class="nx"&gt;check&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt; &lt;span class="nx"&gt;into&lt;/span&gt; &lt;span class="nx"&gt;your&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="nx"&gt;control&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;For&lt;/span&gt; &lt;span class="nx"&gt;more&lt;/span&gt; &lt;span class="nx"&gt;information&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;see&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//aka.ms/azadsp-cli&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"appId"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"displayName"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"spacelift-sp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"http://spacelift-sp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"password"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"A4xFo3z-Hv9BMOFlmHfXP2ESXugHMdaike"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"tenant"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command creates a new Service Principal called &lt;code&gt;spacelift-sp&lt;/code&gt; , and grants it the &lt;em&gt;Contributor&lt;/em&gt; role on your subscription. It also outputs the &lt;code&gt;appId&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt; and &lt;code&gt;tenant&lt;/code&gt; for the Service Principal. Make a note of these because you’ll need them later.&lt;/p&gt;

&lt;p&gt;If you would rather assign permissions yourself later, you can run the following command to just create a Service Principal with no permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;az&lt;/span&gt; &lt;span class="nx"&gt;ad&lt;/span&gt; &lt;span class="nx"&gt;sp&lt;/span&gt; &lt;span class="nx"&gt;create-for-rbac&lt;/span&gt; &lt;span class="nx"&gt;--name&lt;/span&gt; &lt;span class="nx"&gt;spacelift-sp&lt;/span&gt; &lt;span class="nx"&gt;--skip-assignment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should now be able to view your Service Principal in the &lt;em&gt;Azure Active Directory -&amp;gt; App registrations&lt;/em&gt; section of the Azure Portal:&lt;/p&gt;

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

&lt;h1&gt;
  
  
  Creating a Stack
&lt;/h1&gt;

&lt;p&gt;Now that we’ve got a service principal, we need to create a Stack to actually create some Azure resources. To keep things simple all we’re going to do is add some terraform definitions that specify the version of the Azure Provider we want to use, and create an Azure Resource Group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/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;"=2.46.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Configure the Microsoft Azure Provider&lt;/span&gt;
&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"azurerm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;features&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_resource_group"&lt;/span&gt; &lt;span class="s2"&gt;"test-group"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"test-group"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"West Europe"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you commit that change and try to run your Stack just now, you should see something similar to the following failure:&lt;/p&gt;

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

&lt;p&gt;Here’s the full error message:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Error building AzureRM Client: obtain subscription() from Azure CLI: Error parsing json result from the Azure CLI: Error launching Azure CLI: exec: “az”: executable file not found in $PATH&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At first glance this error is slightly confusing, but what it means is that because we didn’t provide any credentials to the Azure provider it attempted to fallback to using the &lt;code&gt;az&lt;/code&gt; CLI for authentication. This works fine locally, but won’t work on Spacelift.&lt;/p&gt;

&lt;p&gt;To solve this, we need to provide our stack with our credentials, which is explained in the next section.&lt;/p&gt;

&lt;h1&gt;
  
  
  Passing Credentials to the Stack
&lt;/h1&gt;

&lt;p&gt;If you take a look at the &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure" rel="noopener noreferrer"&gt;Terraform documentation&lt;/a&gt;, you’ll find that there are a number of different ways that you can authenticate Terraform with Azure. What we’re going to do here is authenticate using a Service Principal with a Client Secret, and provide that configuration via environment variables.&lt;/p&gt;

&lt;p&gt;To do this we need to set the following environment variables, using the information returned by the Azure CLI when we created our Service Principal:&lt;/p&gt;

&lt;p&gt;*ARM_CLIENT_ID — the &lt;code&gt;appId&lt;/code&gt; from the response.&lt;br&gt;
*ARM_CLIENT_SECRET — the &lt;code&gt;password&lt;/code&gt; from the response.&lt;br&gt;
*ARM_SUBSCRIPTION_ID — your subscription ID.&lt;br&gt;
*ARM_TENANT_ID — the &lt;code&gt;tenant&lt;/code&gt; from the response.&lt;/p&gt;

&lt;p&gt;You can set these environment variables directly on your Stack, but what we’re going to do here is create a &lt;a href="https://docs.spacelift.io/concepts/configuration/context" rel="noopener noreferrer"&gt;Spacelift Context&lt;/a&gt; and attach it to our Stack. This allows us to share the same credentials with multiple Stacks if we want.&lt;/p&gt;

&lt;p&gt;Add a new Context via the Contexts screen in Spacelift:&lt;/p&gt;

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

&lt;p&gt;Once your context has been created, edit it and enter your environment variables, making sure to mark the &lt;code&gt;ARM_CLIENT_SECRET&lt;/code&gt; as a secret rather than plaintext:&lt;/p&gt;

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

&lt;p&gt;Now that we’ve got a Context containing our credentials, you can attach it to your Stack. To do this, click on the &lt;em&gt;Manage&lt;/em&gt; link next to your Stack, go to &lt;em&gt;Contexts&lt;/em&gt;, choose your Context, and click &lt;em&gt;Attach&lt;/em&gt;:&lt;/p&gt;

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

&lt;p&gt;Now that your Context is attached, you should be able to see the it in the &lt;em&gt;Environment&lt;/em&gt; section of your Stack:&lt;/p&gt;

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

&lt;p&gt;If you try to run your Azure Stack again you should get a successful plan:&lt;/p&gt;

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

&lt;p&gt;And applying should now succeed:&lt;/p&gt;

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

&lt;p&gt;If you take a look in the Azure Portal, you should now be able to see the Resource Group that was created:&lt;/p&gt;

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

&lt;h1&gt;
  
  
  Credential Expiry
&lt;/h1&gt;

&lt;p&gt;Before finishing up, I wanted to just point out a few things about the Service Principal credential expiry. Although this isn’t really related to Spacelift, it’s something that you need to be aware of.&lt;/p&gt;

&lt;p&gt;When you create your Service Principal via the Azure CLI, it automatically creates a Client Secret for you with an expiry of 1 year:&lt;/p&gt;

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

&lt;p&gt;What this means is that if you do absolutely nothing, your Spacelift Stacks will stop working after the credentials expire a year later. To solve this, all you need to do is generate a new Client Secret, and update the &lt;code&gt;ARM_CLIENT_SECRET&lt;/code&gt; environment variable in your Context.&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrapping Up
&lt;/h1&gt;

&lt;p&gt;Hopefully in this post I’ve shown that it’s simple to manage Azure resources using Spacelift. Once you’ve performed a little bit of initial setup, you should be able to rapidly get new Stacks up and running creating Azure resources.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>azure</category>
      <category>cloud</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Building a Secure CI/CD Integration with Azure</title>
      <dc:creator>Adam Connelly</dc:creator>
      <pubDate>Thu, 21 Oct 2021 12:48:19 +0000</pubDate>
      <link>https://forem.com/spacelift/building-a-secure-cicd-integration-with-azure-2km6</link>
      <guid>https://forem.com/spacelift/building-a-secure-cicd-integration-with-azure-2km6</guid>
      <description>&lt;p&gt;Securing your infrastructure is critical for the success of your business. Failure to take security seriously can result in major damage including fines, loss of customer confidence, or the inability to carry out crucial business functions. The growth of Infrastructure as Code tools and CI/CD systems has allowed developers to integrate infrastructure management into our typical development workflows, improving quality and delivery speed.&lt;/p&gt;

&lt;p&gt;At the same time, in order to manage your infrastructure, the CI/CD system used needs access to sensitive credentials. At Spacelift, we aim to give our users the maximum balance between flexibility and security. Because of this, we provide multiple options for connecting your Azure subscriptions to Azure, including setting static credentials, using our fully managed integration, as well as utilizing private workers to avoid sharing credentials at all.&lt;/p&gt;

&lt;p&gt;In this post I wanted to give you an overview of how the &lt;a href="https://docs.spacelift.io/integrations/cloud-providers/azure#spacelift-managed-integration" rel="noopener noreferrer"&gt;Spacelift Azure integration&lt;/a&gt; works from a technical perspective, as well as discuss some of the issues we encountered and solved while designing and developing it. As a CI/CD system, we do quite a lot of work to integrate with other systems that our users use. Occasionally, like in the case of Azure, things get non-trivial. If you’re keen to learn more, read on, no Azure knowledge required! &lt;/p&gt;

&lt;h1&gt;
  
  
  Concept
&lt;/h1&gt;

&lt;p&gt;Let’s start with a quick description of how our &lt;a href="https://docs.spacelift.io/integrations/cloud-providers" rel="noopener noreferrer"&gt;cloud integrations&lt;/a&gt; work. The overall workflow is very simple, and looks something like this:&lt;/p&gt;

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

&lt;p&gt;Breaking the requirements down, we need to be able to get management credentials for a user’s cloud provider account and pass them to Terraform via environment variables, at which point Terraform can run an apply with access to the customer’s infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For simplicity this post uses Terraform in all its examples, but this overall approach also applies to the other tools that we support, for example, &lt;a href="https://docs.spacelift.io/vendors/pulumi" rel="noopener noreferrer"&gt;Pulumi&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The concept behind the Azure integration was to provide a similar experience to our &lt;a href="https://docs.spacelift.io/integrations/cloud-providers/aws" rel="noopener noreferrer"&gt;AWS&lt;/a&gt; and &lt;a href="https://docs.spacelift.io/integrations/cloud-providers/gcp" rel="noopener noreferrer"&gt;GCP&lt;/a&gt; integrations, but for our Azure customers. The following diagram shows a simplified outline of how the AWS integration works:&lt;/p&gt;

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

&lt;p&gt;AWS provides the ability to temporarily &lt;a href="https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html" rel="noopener noreferrer"&gt;assume a role&lt;/a&gt; in another AWS account. This allows our users to create a role in IAM with any permissions they might want to give Spacelift. They can then set up a trust relationship for this role with our AWS account, which will allow our AWS account to assume the role. Role assumption provides us with raw AWS credentials and works seamlessly with any AWS tooling, including the Terraform AWS provider. It additionally allows us to specify the validity duration, so each run can get its own credentials which are constrained to a short period of time. &lt;/p&gt;

&lt;p&gt;Although Azure doesn’t have the same capability, it provides another approach called Azure Active Directory Applications, that allow service accounts to be created. Azure AD Applications are the resources that allow Spacelift to seamlessly manage access to a customer’s Azure resources.&lt;/p&gt;

&lt;h1&gt;
  
  
  Azure Terminology
&lt;/h1&gt;

&lt;p&gt;It’s worth explaining a few pieces of terminology that are used throughout the rest of this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://azure.microsoft.com/en-gb/services/active-directory/" rel="noopener noreferrer"&gt;Azure Active Directory&lt;/a&gt; (Azure AD) – the identity and access management component of Azure.&lt;/li&gt;
&lt;li&gt;Directory / tenant – an individual instance of Azure AD owned by a company or individual.&lt;/li&gt;
&lt;li&gt;Subscription – the container for any Azure compute resources. This roughly corresponds to an AWS Account. A subscription is linked to a single Azure AD tenant, but multiple subscriptions can be linked to the same tenant.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-how-applications-are-added" rel="noopener noreferrer"&gt;Azure AD Application&lt;/a&gt; – a way of creating external integrations with Azure AD.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/what-is-application-management" rel="noopener noreferrer"&gt;Enterprise Application&lt;/a&gt; – an instance of an Azure AD application that has been installed in another user’s Azure AD tenant.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals#service-principal-object" rel="noopener noreferrer"&gt;Service Principal&lt;/a&gt; – a service account that is automatically created when an Azure AD application is installed. This can be used to grant permissions that allow the application to manage Azure resources.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/en-us/graph/" rel="noopener noreferrer"&gt;Microsoft Graph API&lt;/a&gt; – the main API for managing Azure AD resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Goals
&lt;/h1&gt;

&lt;p&gt;We set a number of goals for the integration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Making it really easy for customers to manage Azure infrastructure using Spacelift.&lt;/li&gt;
&lt;li&gt;Automatic handling of credential rotation so that customers don’t have to deal with this themselves, or use very long-lived credentials to avoid it entirely.&lt;/li&gt;
&lt;li&gt;Providing a mechanism for customers to configure granular permissions in Azure for different stacks, or different types of runs (e.g. PRs vs Tracked Runs).&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Initial Approach
&lt;/h1&gt;

&lt;p&gt;Initially, our idea was to create a single &lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/single-and-multi-tenant-apps" rel="noopener noreferrer"&gt;multi-tenant AD Application:&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The idea was that we would generate an Access Token that could only be used for a specific customer directory, and pass that token to the Terraform Azure RM provider during runs. In the end, we had to revise our approach because of the following issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs" rel="noopener noreferrer"&gt;Terraform Azure RM provider&lt;/a&gt; doesn’t support authentication via an Access Token. Instead, you have to supply the underlying credentials for the account – either a Client Secret or a Client Certificate. In our case, that would have meant passing the credentials for our own multi-tenant application to Spacelift runs. Since that application would have been installed in the Azure AD tenants of any Spacelift user who had setup the integration, this could have allowed users to access other user’s Azure accounts.&lt;/li&gt;
&lt;li&gt;The integration would have been less flexible. Using a single multi-tenant AD application would have prevented customers from creating more than one Azure integration per Active Directory tenant. The ability to create multiple integrations per tenant is useful because it allows different Azure permissions to be applied to each integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Revised Approach
&lt;/h1&gt;

&lt;p&gt;After days of brainstorming on an alternative approach, we came up with a new architecture. We could programmatically generate a new Azure AD Application on our side for each Azure Integration created by Spacelift users. This way, having access to the credentials for an Azure AD Application would only lead to having access to a single Azure AD Tenant on a user’s side. This approach allows Client Secrets to be passed to Spacelift runs without fear of inter-user permission leakage. The final design ended up as shown in the following diagram:&lt;/p&gt;

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

&lt;p&gt;Applications are installed into a customer’s Active Directory tenant via a process called &lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/grant-admin-consent" rel="noopener noreferrer"&gt;Admin Consent&lt;/a&gt;. After admin consent has been completed, a Service Principal is created in the user’s Azure Active Directory to which the user can grant permissions. This allows users to decide the exact level of access that Spacelift has to their resources.&lt;/p&gt;

&lt;h1&gt;
  
  
  Credential Generation
&lt;/h1&gt;

&lt;p&gt;The next issue we faced was related to generating credentials for a run. As described in the &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret" rel="noopener noreferrer"&gt;provider documentation&lt;/a&gt;, the Azure RM provider can be configured by setting certain environment variables. Initially, we took a basic approach of attempting to generate credentials during a Spacelift run. This is what we do for our AWS and GCP integrations, so we weren’t expecting major issues. The steps taken looked something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run triggered.&lt;/li&gt;
&lt;li&gt;Generate a new Client Secret with a short expiry time.&lt;/li&gt;
&lt;li&gt;Populate the required environment variables.&lt;/li&gt;
&lt;li&gt;Execute terraform.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This seemed to work… but only some of the time.&lt;/p&gt;

&lt;p&gt;While testing the integration, strange things were happening. As an example, the &lt;em&gt;planning&lt;/em&gt; phase for a run would succeed, but the &lt;em&gt;apply&lt;/em&gt; would fail with a permissions error from Azure. After investigating, we came to the conclusion that this was being caused by &lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-architecture" rel="noopener noreferrer"&gt;eventual consistency&lt;/a&gt; in Azure AD.&lt;/p&gt;

&lt;p&gt;You can visualize the problem using the following diagram (note: this is just an illustration, and is not meant to be completely accurate):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsam55nuu8oli0amofzcy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsam55nuu8oli0amofzcy.png" alt="problem with eventual consistency in Azure AD" width="465" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the example above, step 2 may succeed or fail depending on whether the secret has managed to replicate to the Azure AD server that its request is routed to. Initially, we attempted to test whether or not the secret was usable by making an API request, and retrying until the request succeeded using an exponential backoff. What we soon realized was that even then, subsequent requests could be routed to a different Azure AD instance, which still hasn’t received the new Client Secret, and potentially fail.&lt;/p&gt;

&lt;p&gt;Even if it was possible to verify when the secret was fully replicated, waiting for replication to complete would have added a minimum of 30 seconds, and potentially another &lt;strong&gt;several minutes&lt;/strong&gt;. Because of this, we decided to move credential generation and rotation out of the run flow, and into a scheduled task:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffd3rtlrpcdkboiy8f0f9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffd3rtlrpcdkboiy8f0f9.png" alt="moving credential generation and rotation out of the run flow" width="717" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The scheduled task runs once per hour, generates secrets with an expiry of 24 hours, and attempts to generate a new secret for an integration roughly 2 hours before expiry of the old secret. This allows credential rotation while always keeping a valid secret.&lt;/p&gt;

&lt;p&gt;When a new secret is generated, we use AWS’s &lt;a href="https://aws.amazon.com/kms/" rel="noopener noreferrer"&gt;Key Management Service&lt;/a&gt; to encrypt it so that it is never stored in plaintext.&lt;/p&gt;

&lt;p&gt;When a run is triggered, we try to find the secret for the integration with the most amount of time until expiry. We also avoid using new secrets until roughly 10 minutes after generation to avoid the eventual consistency issues caused by Azure AD’s architecture.&lt;/p&gt;

&lt;p&gt;You can visualize the secret lifecycle using the following diagram:&lt;/p&gt;

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

&lt;p&gt;In addition, when creating a new integration we immediately generate a secret. This helps to ensure that the secret will have successfully propagated within Azure AD by the time a run is triggered.&lt;/p&gt;

&lt;h1&gt;
  
  
  Credential Rotation for the Integration
&lt;/h1&gt;

&lt;p&gt;The last major issue we faced was figuring out how to implement credential rotation for our own management account. The integration itself uses an Azure AD Service Principal to manage customer AD Applications using the Microsoft Graph API. Because we run most of our own infrastructure in AWS, we didn’t have the option of using a &lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview" rel="noopener noreferrer"&gt;Managed Service Identity&lt;/a&gt;, meaning that we needed to handle credential rotation ourselves. In addition, our goal was to automate the process to avoid developers having to periodically perform a manual task, and to reduce the risk of forgetting to renew the credentials before expiry.&lt;/p&gt;

&lt;p&gt;In the end, we decided to take the relatively simple approach of storing the certificate in Secrets Manager and writing a scheduled task to periodically check whether the certificate was ready to expire, similar to the approach we took for Client Secrets for the integration. If so the scheduled task generates a new certificate and uploads it to both Secrets Manager and Azure AD:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6n4hc3fyazlpsormpsq7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6n4hc3fyazlpsormpsq7.png" alt="Credential Rotation for the Integration&amp;lt;br&amp;gt;
" width="714" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The parts of the system that need to use the client certificate for authentication periodically check for an updated certificate. As with the client secret rotation, we avoid using the new certificate for approximately 10 minutes to allow time for the certificate to propagate throughout Azure AD.&lt;/p&gt;

&lt;p&gt;Similar to what happens with the integration client secrets, Secrets Manager uses AWS Key Management Service to encrypt the certificate at rest.&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrapping Up
&lt;/h1&gt;

&lt;p&gt;Hopefully, this post has given you a glimpse into the internals of Spacelift’s Azure integration, along with some of the problems we had to solve while implementing it. As you probably noticed, we’re willing to go to great lengths to ensure a secure and pleasant experience for our users. To find out more, take a look at our Azure integration documentation available at &lt;a href="https://docs.spacelift.io/integrations/cloud-providers/azure" rel="noopener noreferrer"&gt;Spacelift Documentation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cicd</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
    <item>
      <title>How does Open Policy Agent (OPA) work?</title>
      <dc:creator>Adam Connelly</dc:creator>
      <pubDate>Tue, 19 Oct 2021 16:32:12 +0000</pubDate>
      <link>https://forem.com/spacelift/how-does-open-policy-agent-work-27kc</link>
      <guid>https://forem.com/spacelift/how-does-open-policy-agent-work-27kc</guid>
      <description>&lt;p&gt;In this article, I want to give an overview of Open Policy Agent, why you would want to use it, as well as showcasing how you can use OPA with your Spacelift account. Although OPA can be used for many purposes, I’m going to focus on how it can be used alongside Infrastructure as Code.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is OPA?
&lt;/h1&gt;

&lt;p&gt;Open Policy Agent provides a way of declaratively writing policies as code and then using those policies as part of a decision-making process. It uses a policy language called Rego, allowing you to write policies for various different services using the same language.&lt;/p&gt;

&lt;p&gt;OPA can be used for a number of purposes, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authorization of REST API endpoints.&lt;/li&gt;
&lt;li&gt;Allowing or denying Terraform changes based on compliance or safety rules.&lt;/li&gt;
&lt;li&gt;Integrating custom authorization logic into applications.&lt;/li&gt;
&lt;li&gt;Implementing Kubernetes Admission Controllers to validate API requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OPA was originally created by &lt;a href="https://www.styra.com/" rel="noopener noreferrer"&gt;Styra&lt;/a&gt;, and is now a part of the Cloud Native Computing Foundation (CNCF), alongside other CNCF technologies like Kubernetes and Prometheus.&lt;/p&gt;

&lt;h1&gt;
  
  
  How does OPA work?
&lt;/h1&gt;

&lt;p&gt;We can visualise how OPA works using the following diagram:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmj7d6q9ibnd1h1k7zs6e.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmj7d6q9ibnd1h1k7zs6e.jpg" alt="How does OPA work" width="561" height="261"&gt;&lt;/a&gt;&lt;br&gt;
As you can see, OPA accepts a policy, input and query, and generates a response based on that. The input can be any valid JSON document, allowing OPA to integrate with any tool that produces JSON output.&lt;/p&gt;
&lt;h1&gt;
  
  
  Why would I want to use it?
&lt;/h1&gt;

&lt;p&gt;In the following sections, I’ll go into more specific examples of using OPA that should help make things clearer, but before I do here’s a quick list of reasons that I’m interested in using OPA:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Policies as code allow you to follow your standard development lifecycle with PRs, CI, etc, and provide you with a history of changes to your policies.&lt;/li&gt;
&lt;li&gt;OPA is designed to work with any kind of JSON input, meaning it can easily integrate with any tool that produces JSON output.&lt;/li&gt;
&lt;li&gt;Because OPA integrates with a number of different tools, it allows you to use a standard policy language across many parts of your system, rather than relying on multiple vendor-specific technologies.&lt;/li&gt;
&lt;li&gt;OPA supports unit-testing, making it easier and faster to iterate your policies with confidence that they won’t break.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  Using OPA with Terraform
&lt;/h1&gt;

&lt;p&gt;Let’s try to make that a bit less theoretical by using a specific example: &lt;em&gt;Terraform&lt;/em&gt;. Terraform can produce a plan in JSON format via the terraform show command. This means that we can  define policies for our infrastructure, and use OPA to make a decision about whether a plan is safe to apply or not:&lt;/p&gt;

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

&lt;p&gt;For example, say we have the following terraform definition to create an EC2 instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-central-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;ami&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ami-00003c1d"&lt;/span&gt;
 &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now say we want to ensure that every Terraform resource has a Name tag, we could enforce that by creating a file called &lt;em&gt;plan.rego&lt;/em&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;package&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;

&lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;resource_change&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_changes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

   &lt;span class="nx"&gt;resource_change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;after&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="s2"&gt;"Name"&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 order to use OPA to evaluate our policy, we need to take the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a Terraform plan as JSON.&lt;/li&gt;
&lt;li&gt;Run opa eval to verify whether that plan passes our policy or not.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Step 1 – Generate our Terraform plan as JSON
&lt;/h1&gt;

&lt;p&gt;To get a JSON representation of our plan, we need to output our plan to a file, and then use the terraform show command to output that plan as JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="nx"&gt;-out&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;
&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="nx"&gt;show&lt;/span&gt; &lt;span class="nx"&gt;-json&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Step 2 – Run opa eval
&lt;/h1&gt;

&lt;p&gt;We can then use opa eval to evaluate our plan against our policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;opa&lt;/span&gt; &lt;span class="nx"&gt;eval&lt;/span&gt; &lt;span class="nx"&gt;--data&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rego&lt;/span&gt; &lt;span class="nx"&gt;--input&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="s2"&gt;"data.spacelift.allow"&lt;/span&gt;
&lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we’re using the query data.spacelift.allow because our policy is loaded as a data file, and we defined our allow rule in the spacelift namespace. You can also see that opa eval produced empty output ({}). This means that our allow rule didn’t evaluate to true, and so produced no output.&lt;/p&gt;

&lt;p&gt;Let’s adjust our terraform definition to include a Name tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;ami&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ami-00003c1d"&lt;/span&gt;
 &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;

 &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-instance"&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;Now if we generate our plan and evaluate the policy again, we should get a slightly different output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="nx"&gt;-out&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
 &lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="nx"&gt;show&lt;/span&gt; &lt;span class="nx"&gt;-json&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="nx"&gt;lots&lt;/span&gt; &lt;span class="nx"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;Terraform&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;

&lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;opa&lt;/span&gt; &lt;span class="nx"&gt;eval&lt;/span&gt; &lt;span class="nx"&gt;--data&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rego&lt;/span&gt; &lt;span class="nx"&gt;--input&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="s2"&gt;"data.spacelift.allow"&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="s2"&gt;"result"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="s2"&gt;"expressions"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
       &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"data.spacelift.allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="s2"&gt;"location"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="s2"&gt;"row"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s2"&gt;"col"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
         &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells us that the allow rule has evaluated true. We can get that in a slightly more concise way using the &lt;em&gt;pretty&lt;/em&gt; format option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;opa&lt;/span&gt; &lt;span class="nx"&gt;eval&lt;/span&gt; &lt;span class="nx"&gt;--data&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rego&lt;/span&gt; &lt;span class="nx"&gt;--input&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="nx"&gt;--format&lt;/span&gt; &lt;span class="nx"&gt;pretty&lt;/span&gt; &lt;span class="s2"&gt;"data.spacelift.allow"&lt;/span&gt;
&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this stage, you can probably imagine how you could integrate this into your CI/CD pipeline to enforce naming schemes, security rules, and various other organizational policies.&lt;/p&gt;

&lt;h1&gt;
  
  
  Other Examples
&lt;/h1&gt;

&lt;p&gt;The following examples illustrate some possible use-cases for OPA with Terraform. All of the examples have been taken from the Spacelift &lt;a href="https://docs.spacelift.io/concepts/policy/terraform-plan-policy#cookbook" rel="noopener noreferrer"&gt;plan policy documentation&lt;/a&gt;, but have been altered to work with plain vanilla Terraform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1. Require human review when resources are deleted or updated
&lt;/h2&gt;

&lt;p&gt;Adding new resources is usually a fairly safe operation, but updating or deleting existing resources can carry more risk. You could flag these for human review using the following policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;package&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;

&lt;span class="nx"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;  &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"action '%s' requires human review (%s)"&lt;/span&gt;
  &lt;span class="nx"&gt;review&lt;/span&gt;   &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"delete"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_changes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;action&lt;/span&gt;   &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;action&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;
  
  
  Example 2. Require commits to be reasonably sized
&lt;/h2&gt;

&lt;p&gt;Once a PR goes over a certain size it becomes difficult to review without missing things. The same is true for Terraform plans. The following policy can be used to warn when the number of changes goes over a certain threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;package&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;

&lt;span class="nx"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;too_many_changes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&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;too_many_changes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
   &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_changes&lt;/span&gt;
   &lt;span class="nx"&gt;ret&lt;/span&gt; &lt;span class="err"&gt;:&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;r&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="err"&gt;:=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="err"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"no-op"&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
   &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"more than %d changes (%d)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
   &lt;span class="nx"&gt;ret&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example 3. Blast radius
&lt;/h2&gt;

&lt;p&gt;The following policy attempts to determine the risk of a particular plan by assigning different weightings to the change type (create, update, delete), along with the affected resource type (ECS cluster, EC2 instance, etc). It takes the approach that an update or delete is more risky than a create because it affects an existing resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;package&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;

&lt;span class="nx"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;blast_radius_too_high&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&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;blast_radius_too_high&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"change blast radius too high (%d/100)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;blast_radius&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;blast_radius&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;blast&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;
                        &lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="err"&gt;:=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_changes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt;
                        &lt;span class="nx"&gt;blast&lt;/span&gt; &lt;span class="err"&gt;:=&lt;/span&gt; &lt;span class="nx"&gt;blast_radius_for_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;

   &lt;span class="nx"&gt;blast_radius&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;   
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;blast_radius_for_resource&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ret&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;blasts_radii_by_action&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"delete"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"update"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"create"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"no-op"&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="nx"&gt;ret&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="err"&gt;:=&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                   &lt;span class="nx"&gt;action_impact&lt;/span&gt; &lt;span class="err"&gt;:=&lt;/span&gt; &lt;span class="nx"&gt;blasts_radii_by_action&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                   &lt;span class="nx"&gt;type_impact&lt;/span&gt; &lt;span class="err"&gt;:=&lt;/span&gt; &lt;span class="nx"&gt;blast_radius_for_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resource&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;value&lt;/span&gt; &lt;span class="err"&gt;:=&lt;/span&gt; &lt;span class="nx"&gt;action_impact&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;type_impact&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Let's give some types of resources special blast multipliers.&lt;/span&gt;
&lt;span class="nx"&gt;blasts_radii_by_type&lt;/span&gt; &lt;span class="err"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_cluster"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_user"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_role"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# By default, blast radius has a value of 1.&lt;/span&gt;
&lt;span class="nx"&gt;blast_radius_for_type&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;blasts_radii_by_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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;blast_radius_for_type&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ret&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;blasts_radii_by_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="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ret&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Unit Testing
&lt;/h1&gt;

&lt;p&gt;How can we make sure that our policies work as we expect and that they don’t break over time as we make changes to them? You guessed it: unit testing! Luckily for us, OPA has first-class support for testing via the opa &lt;code&gt;test&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;To create tests for our policy, all we need to do is create another Rego file with a series of rules prefixed with &lt;code&gt;test_&lt;/code&gt;. Each rule starting with that prefix defines a separate test.&lt;/p&gt;

&lt;p&gt;Let’s go ahead and create a file called &lt;em&gt;plan_test.rego&lt;/em&gt;, with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;package&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;

&lt;span class="nx"&gt;test_allow_missing_name_tag&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="nx"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="nx"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="s2"&gt;"resource_changes"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
       &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="s2"&gt;"change"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="s2"&gt;"after"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
             &lt;span class="s2"&gt;"tags"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="p"&gt;},&lt;/span&gt;
         &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="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;As you can see, OPA makes it really easy to specify the policy input using the &lt;code&gt;&amp;lt;variable&amp;gt;&lt;/code&gt; as &lt;code&gt;&amp;lt;value&amp;gt;&lt;/code&gt; syntax. This allows us to create very concise tests by only including values we care about in the policy input, rather than having to use the entire plan output.&lt;/p&gt;

&lt;p&gt;Let’s go ahead and run &lt;code&gt;opa test&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;opa&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spacelift&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test_allow_missing_name_tag&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PASS&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;188.503&lt;/span&gt;&lt;span class="err"&gt;µ&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;--------------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="nx"&gt;PASS&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;




   &lt;span class="nx"&gt;resource_change&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

   &lt;span class="nx"&gt;resource_change&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Environment"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unsurprisingly our test passes. That’s not very exciting, so let’s add a new test to ensure our resources include an &lt;code&gt;Environment&lt;/code&gt; tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;test_allow_missing_environment_tag&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="nx"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="nx"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="s2"&gt;"resource_changes"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
       &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="s2"&gt;"change"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="s2"&gt;"after"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
             &lt;span class="s2"&gt;"tags"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"my-instance"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
           &lt;span class="p"&gt;},&lt;/span&gt;
         &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="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;Running opa test again shows us a failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;opa&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spacelift&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test_allow_missing_environment_tag&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FAIL&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;118.078&lt;/span&gt;&lt;span class="err"&gt;µ&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;--------------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="nx"&gt;PASS&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="nx"&gt;FAIL&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which we can fix by updating our policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;resource_change&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_changes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="nx"&gt;resource_change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;after&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="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="nx"&gt;resource_change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;after&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="s2"&gt;"Environment"&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;At this stage if you run the test command again it should show 2 passes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="nx"&gt;opa&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="nx"&gt;PASS&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to know more about OPA testing, the &lt;a href="https://www.openpolicyagent.org/docs/latest/policy-testing/" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; are full of great examples and information about what you can do.&lt;/p&gt;

&lt;h1&gt;
  
  
  OPA + Spacelift
&lt;/h1&gt;

&lt;p&gt;At this stage, hopefully, you’ve got a pretty good idea of what OPA is as well as how it can be useful to you. It’s not too difficult to see how you could start integrating OPA into your development process or even use it as part of production systems.&lt;/p&gt;

&lt;p&gt;Luckily for you, at Spacelift all heavy lifting is done for you, allowing you to get the benefits of using OPA for Policy-as-Code without having to implement everything from scratch for yourself. In this section, I want to showcase some of the functionality that Spacelift provides that relates to OPA.&lt;/p&gt;

&lt;h2&gt;
  
  
  1) Policy Types
&lt;/h2&gt;

&lt;p&gt;Spacelift allows you to use OPA policies to manage various aspects of your Spacelift account, not just during planning. For example, you can use policies to control who can login to your account, along with what they have access to. For more information see &lt;a href="https://docs.spacelift.io/concepts/policy" rel="noopener noreferrer"&gt;https://docs.spacelift.io/concepts/policy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2) PR Checks
&lt;/h2&gt;

&lt;p&gt;Spacelift can automatically trigger planning runs whenever you push changes to your VCS provider. For example, here’s what you might see in GitHub after creating a PR:&lt;/p&gt;

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

&lt;p&gt;If all goes well your check will succeed, but if a policy is violated, a failed check will be reported:&lt;/p&gt;

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

&lt;p&gt;You can then view the details of the failure in Spacelift:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  3) Manual Approvals
&lt;/h2&gt;

&lt;p&gt;Spacelift &lt;a href="https://docs.spacelift.io/concepts/policy/terraform-plan-policy" rel="noopener noreferrer"&gt;plan policies&lt;/a&gt; use a slightly different format than we used in our example policies earlier in this post. Not only do they allow you to specify a message to be displayed, but they also have the concept of &lt;code&gt;deny&lt;/code&gt; and &lt;code&gt;warn&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;package&lt;/span&gt; &lt;span class="nx"&gt;spacelift&lt;/span&gt;

&lt;span class="nx"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"you shall not pass"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hey, you look suspicious"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="kc"&gt;true&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;code&gt;deny&lt;/code&gt; rule fails the run completely, while the &lt;code&gt;warn&lt;/code&gt; rule just displays a warning in the logs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;warn&lt;/code&gt; rules take on another role when a run is going to deploy changes (vs just showing the planned changes against a PR). If any warnings are reported during a deployment, the run will wait for manual approval before applying any changes.&lt;/p&gt;

&lt;p&gt;Let’s use the following example policy, designed to warn if a certain set of suggested tags aren’t found on our resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;suggested_tags&lt;/span&gt; &lt;span class="err"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Environment"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"resource %q does not have all suggested tags (%s)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;concat&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;missing_tags&lt;/span&gt;&lt;span class="p"&gt;)])]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_changes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;

 &lt;span class="nx"&gt;missing_tags&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;suggested_tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;]&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;missing_tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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 then attempt to add resources that don’t contain all of those tags, Spacelift will block before applying the changes, and give us the chance to manually approve or deny the run:&lt;/p&gt;

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

&lt;p&gt;This allows you to build complex workflows where certain changes are completely blocked, but others are allowed as long as the changes are reviewed first.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://docs.spacelift.io/concepts/policy/terraform-plan-policy#cookbook" rel="noopener noreferrer"&gt;plan policy cookbook&lt;/a&gt; for more ideas about what you can do.&lt;/p&gt;

&lt;h1&gt;
  
  
  Spacelift Terraform Provider
&lt;/h1&gt;

&lt;p&gt;Spacelift provides a &lt;a href="https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs" rel="noopener noreferrer"&gt;Terraform provider&lt;/a&gt; for managing your Spacelift account. This means that you can manage the policies available within your account, along with the Stacks they apply to in code.&lt;/p&gt;

&lt;p&gt;For example, to add the policy we defined earlier to Spacelift we can use the &lt;code&gt;spacelift_policy&lt;/code&gt; resource like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"spacelift_policy"&lt;/span&gt; &lt;span class="s2"&gt;"plan"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PLAN"&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;"Plan Policy"&lt;/span&gt;
 &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${path.module}/plan.rego"&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;We can then attach this policy to a Spacelift Stack using the spacelift_policy_attachment resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"spacelift_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"mystack-plan"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;policy_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;spacelift_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&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;stack_id&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;spacelift_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mystack&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Taking this a step further, we could create a custom module for defining Spacelift Stacks that ensured that all Stacks had a certain set of policies attached by default.&lt;/p&gt;

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

&lt;p&gt;I hope you’ve enjoyed this post, and can see the value that OPA brings to the table. If you’re interested in trying out Spacelift to see what it has to offer, why not sign up for a &lt;a href="https://spacelift.io/free-trial" rel="noopener noreferrer"&gt;free trial&lt;/a&gt;? You can setup a Spacelift account in minutes, and get started on your Open Policy Agent journey!&lt;/p&gt;

</description>
      <category>opa</category>
      <category>openpolicyagent</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to Manage Active Directory Objects with Azure AD Provider for Terraform</title>
      <dc:creator>Adam Connelly</dc:creator>
      <pubDate>Mon, 18 Oct 2021 17:16:58 +0000</pubDate>
      <link>https://forem.com/spacelift/how-to-manage-active-directory-objects-with-azure-ad-provider-for-terraform-9e2</link>
      <guid>https://forem.com/spacelift/how-to-manage-active-directory-objects-with-azure-ad-provider-for-terraform-9e2</guid>
      <description>&lt;p&gt;The &lt;a href="https://registry.terraform.io/providers/hashicorp/azuread/latest/docs" rel="noopener noreferrer"&gt;Azure AD provider for Terraform&lt;/a&gt; can be used to manage your Azure Active Directory resources declaratively. This allows you to do things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatically provision users and make sure they belong to the correct groups.&lt;/li&gt;
&lt;li&gt;Manage Azure compute permissions via Azure AD groups.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Example Usage
&lt;/h1&gt;

&lt;p&gt;The following example shows how to use the Azure AD provider to create a group in Azure AD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;azuread&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/azuread"&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.6.0"&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_group"&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Test Group"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terraform section at the start is used to specify the version of the provider that we want to use, and the resource &lt;code&gt;azuread_group&lt;/code&gt; &lt;code&gt;test&lt;/code&gt; block defines our group.&lt;/p&gt;

&lt;h1&gt;
  
  
  Authenticating with Azure
&lt;/h1&gt;

&lt;p&gt;The Azure AD provider allows multiple authentication methods, which are outlined in the provider's documentation. To allow you to get up and running quickly, the AD provider will attempt to get your credentials via the Azure CLI.&lt;/p&gt;

&lt;p&gt;While this is fine for experimentation and local testing, for non-interactive scenarios like CI you need to use a &lt;em&gt;Service Principal&lt;/em&gt; or a &lt;em&gt;Managed Service Identity&lt;/em&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Permissions
&lt;/h1&gt;

&lt;p&gt;In order to manage your Azure AD objects, the account used by Terraform needs to have the correct permissions to perform its actions. You can manage these permissions via the &lt;em&gt;Roles and administrators&lt;/em&gt; section of Azure AD:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0onqbhu2dzyem75c9l0t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0onqbhu2dzyem75c9l0t.jpg" alt="Active Directory Objects with Azure AD Provider" width="800" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example, to allow a Service Principal to manage groups, you would add it to the &lt;em&gt;Groups administrator&lt;/em&gt; role:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbggh8oethir4kpvfuob.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbggh8oethir4kpvfuob.jpg" alt="Active Directory Objects with Azure AD Provider" width="800" height="191"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  API Permissions
&lt;/h1&gt;

&lt;p&gt;Another option that can be used with Service Principals instead of granting an administrator role is to grant specific API permissions to them. To do this, first find the AD Application linked to your Service Principal in the &lt;em&gt;App Registrations&lt;/em&gt; section:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fizymbl2glz03qpqkvl9m.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fizymbl2glz03qpqkvl9m.jpg" alt="Active Directory Objects with Azure AD Provider" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go to the API permissions page for the application, and click on &lt;em&gt;Add a permission&lt;/em&gt;:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34ksnk6phnudhgx4sube.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34ksnk6phnudhgx4sube.jpg" alt="Active Directory Objects with Azure AD Provider" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the screen that appears, choose the &lt;em&gt;Azure Active Directory Graph&lt;/em&gt; API, and then choose the relevant permission you want to add:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgng7dj5utfbh5pr5ho9r.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgng7dj5utfbh5pr5ho9r.jpg" alt="Active Directory Objects with Azure AD Provider" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before the Service Principal can actually use the permission you just added, you need to take a final step called granting &lt;em&gt;Admin Consent&lt;/em&gt;. You can do this by clicking on the Grant admin consent for &lt;em&gt;&lt;/em&gt; button displayed above the permissions table:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fftkkq9n5ny7fs2zo2vq9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fftkkq9n5ny7fs2zo2vq9.jpg" alt="Active Directory Objects with Azure AD Provider" width="800" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTES&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When adding permissions to your Service Principal, you need to add &lt;em&gt;Application permissions&lt;/em&gt; rather than &lt;em&gt;Delegated permissions&lt;/em&gt;. This means that the Service Principal is allowed to perform the specified actions as itself, rather than on behalf of another user.&lt;/li&gt;
&lt;li&gt;The set of permissions that you can add via API permissions is quite limited. For example, to create AD groups you need to add the &lt;em&gt;Directory.ReadWrite.All&lt;/em&gt; permission, but this will not allow your Service Principal to delete any groups it creates. In order to be able to delete groups, you need to grant it the &lt;em&gt;Group Administrator&lt;/em&gt; role, so depending on your requirements there may not be any point in granting API permissions.&lt;/li&gt;
&lt;li&gt;The Azure AD Terraform provider is switching to the &lt;em&gt;Microsoft Graph API&lt;/em&gt; as of version 2.0.0, so after version 2 is released you will need to grant permissions to the &lt;em&gt;Microsoft Graph API&lt;/em&gt; instead of to the &lt;em&gt;Azure Active Directory Graph API&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  More Examples
&lt;/h1&gt;
&lt;h2&gt;
  
  
  Managing Users and Groups
&lt;/h2&gt;

&lt;p&gt;The following example creates two users and two groups, and assigns each user to a group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_user"&lt;/span&gt; &lt;span class="s2"&gt;"adamc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;user_principal_name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"adamc@adamrpconnellygmail.onmicrosoft.com"&lt;/span&gt;
 &lt;span class="nx"&gt;display_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Adam Connelly"&lt;/span&gt;
 &lt;span class="nx"&gt;password&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SuperSecret01@!"&lt;/span&gt;
 &lt;span class="nx"&gt;force_password_change&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_user"&lt;/span&gt; &lt;span class="s2"&gt;"bobd"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;user_principal_name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bobd@adamrpconnellygmail.onmicrosoft.com"&lt;/span&gt;
 &lt;span class="nx"&gt;display_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Bob Dolton"&lt;/span&gt;
 &lt;span class="nx"&gt;password&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SuperSecret01@!"&lt;/span&gt;
 &lt;span class="nx"&gt;force_password_change&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_group"&lt;/span&gt; &lt;span class="s2"&gt;"development"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Development"&lt;/span&gt;
 &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="nx"&gt;azuread_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adamc&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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_group"&lt;/span&gt; &lt;span class="s2"&gt;"sales"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Sales"&lt;/span&gt;
 &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="nx"&gt;azuread_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bobd&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating a Service Principal and granting RBAC permissions
&lt;/h2&gt;

&lt;p&gt;The following example combines the &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs" rel="noopener noreferrer"&gt;Azure AD provider&lt;/a&gt;&lt;br&gt;
with the Azure RM provider, allowing you to create a Service Principal and assign it permission to manage certain Azure resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create an AD Application&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_application"&lt;/span&gt; &lt;span class="s2"&gt;"automation"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sp-automation"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Create a Service Principal from that Application&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_service_principal"&lt;/span&gt; &lt;span class="s2"&gt;"automation"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;application_id&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azuread_application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;automation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_id&lt;/span&gt;
 &lt;span class="nx"&gt;app_role_assignment_required&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Get information about the configured Azure subscription&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_subscription"&lt;/span&gt; &lt;span class="s2"&gt;"primary"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;# Grant our service principal "Contributor" access over the subscription&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_role_assignment"&lt;/span&gt; &lt;span class="s2"&gt;"automation_contributor"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;scope&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azurerm_subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&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;role_definition_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Contributor"&lt;/span&gt;
 &lt;span class="nx"&gt;principal_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azuread_service_principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;automation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this post I’ve covered what the Azure AD Terraform provider is used for, how to authenticate and grant the correct permissions, as well as showing a few examples of what you can do with it. Hopefully you’ve found it useful!&lt;/p&gt;

&lt;p&gt;If you’re interested in finding out about how you can use Spacelift to manage your Azure resources, check out &lt;a href="https://docs.spacelift.io/integrations/cloud-providers/azure" rel="noopener noreferrer"&gt;Spacelift Azure documentation&lt;/a&gt;. Also, don’t forget that you can easily give Spacelift a &lt;a href="https://spacelift.io/free-trial" rel="noopener noreferrer"&gt;free test drive&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;You will find more &lt;a href="https://spacelift.io/blog/what-is-terraform" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; Tutorials on our website:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/importing-exisiting-infrastructure-into-terraform" rel="noopener noreferrer"&gt;How to Import Existing Infrastructure into Terraform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/what-are-terraform-modules-and-how-do-they-work" rel="noopener noreferrer"&gt;How to Use Terraform Modules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/how-to-use-terraform-variables" rel="noopener noreferrer"&gt;How to Use Terraform Variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/terraform-functions-expressions-loops" rel="noopener noreferrer"&gt;How to Use Terraform Functions, Expression and Loops&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/how-to-destroy-terraform-resources" rel="noopener noreferrer"&gt;How to Destroy Resources from Terraform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/terraform-version-upgrade" rel="noopener noreferrer"&gt;How to Upgrade Terraform to the Latest Version&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/5-ways-to-manage-terraform-at-scale" rel="noopener noreferrer"&gt;5 Ways to Manage Terraform at Scale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/how-to-provision-aws-eks-kubernetes-cluster-with-terraform" rel="noopener noreferrer"&gt;How to Provision an AWS EKS Kubernetes Cluster with Terraform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spacelift.io/blog/getting-started-with-terraform-on-google-cloud-platform-gcp" rel="noopener noreferrer"&gt;How to Get Started with Terraform on Google Cloud Platform&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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