Skip to content

Latest commit

 

History

History
1262 lines (775 loc) · 49.1 KB

File metadata and controls

1262 lines (775 loc) · 49.1 KB

Part 1 - Introduction to IaC

In this first part, you will learn about Infrastructure as Code (IaC) and how to work with Bicep or Terraform to create a simple resource group and storage account. You'll also learn about things like:

  • running deployments from the command line
  • running deployments from GitHub Actions
  • building service principals with federated credentials

Prerequisites

To complete this activity, you must have an editor like VSCode, an Azure Subscription with contributor access, and the Azure CLI installed.

Part 1: Introduction to IaC with Terraform

Apart from the common concepts of deployment scopes and resource groups there are Terraform specific features that are key to deploying resources. We will explore those in this section:

Providers

A provider is a plugin that allows Terraform to interact with cloud, SaaS providers and other APIs. Custom terraform providers can be created if needed. Example of providers are:

  • Azure, Google and AWS providers.
  • MongoDB, Pager Duty.
  • Random, arm2tf.

Basic file structure

This is the recommended file structure for a working directory:

  • providers.tf: Specifies the providers used in the deployment as well as any configuration for each of them.
  • main.tf: Specified the resources being deployed.
  • variables.tf: Specifies the variables that will be used to parameterize the deployment.
  • outputs.tf Specified any values that will be availabe as an output of the deployment.

You can also use the main.tf file to define all the elements mentioned above, however, that will make your deployments harder to read and maintain.

Commands

There are 3 main commands that we will explore in this section:

  • terraform init: Initialized the working directory.
  • terraform plan: Creates a plan based on the resources specified in your deployment and the resources currently deployed.
  • terraform apply: Applies the plan generated by the plan command.

Task 1 - Create your first Terraform file to deploy a storage account to an existing resource group

To get started, let's create our first Terraform file. The overall goal for this activity is to create the files needed to deploy a storage account. During this activity we will create the recommended file structure mentioned above while learning about using variables and outputs, as well as how to create and use additional files as modules.

Note: for this activity, I'm using VSCode with the Terraform extension. Additionally, I've created a new repository at GitHub which has the starter web application code in it and will be where I'm generating screenshots. For this reason, if you haven't already, you need a GitHub repository where you can store your code and your Terraform files. For simplicity, you can fork this repo: https://github.com/AzureCloudWorkshops/ACW-InfrastructureAsCode_Workshop.

A good way to store this would be similar to the following:

"Initial repo with folder structure"

Step 1 - Create your file main.tf

Start by creating a main.tf file. This can be done in a bash terminal, in VSCode, or in PowerShell.

  1. Create a folder if you don't have one for iac and a subfolder Terraform. In the terraform subfolder, create a file main.tf.

    Note: If you forked the repo above, you will already have an iac folder in that repo.

    Folder:

    mkdir terraform
    cd terraform
    mkdir Part1_Main_Module
    cd iac\terraform\Part1_Main_Module
  2. Copy or create the main.tf file in the Part1_Main_Module folder:

    Create the file Bash:

    touch main.tf

    or PowerShell:

    "" > "main.tf"

    or use VSCode:

    Right-click on the folder and select New File, name it `main.tf`
    

    Note: For Bash and PowerShell, make sure you make directories mkdir and change directories cd to the correct location. For VSCode, you can right-click on the folder and select New File, name it main.tf.

Step 1 Completion Check

Before moving on, ensure that you have a file called main.tf in a folder called iac\terraform\Part1_Main_Module at the root of your repository (the _workshop repo has starter files and/or you should have made a folder with the main.tf file as shown above).

Step 2 - Create the terraform code to create a storage account

For this first activity, you'll be creating a simple storage account. To do this easily, you'll want a couple of extensions for Terraform in place in VSCode:

  1. Get the Hashicorp Terraform Extension

    Terraform: "The VSCode Terraform Extension is shown"

  2. Get the Azure Tools Extension (optional/recommended)

    Azure Tools: "The VSCode Azure Tools Extension is shown"

    Note: Azure Tools may not be needed, but it's a good idea to have it in place for other things you will do in the future.

  3. Specify the providers that we will use in the deployment.

    Add the following code to your main.tf file:

    terraform {
      required_version = ">=1.6.6"
    
      required_providers {
        azurerm = {
          source  = "hashicorp/azurerm"
          version = "~>3.0"      
        }    
      }
    }
    
    provider "azurerm" {
      features {    
      }
    }
    

Step 2 Completion Check

Before moving on, ensure that you have the main.tf file in the correct folder with the settings as above.

Step 3 - Create the storage account resource

In this step you'll create the storage account resource

  1. Add a resource block to create the storage account:

    Using the text below, change the name of the storage account to something that is unique, such as iacstgacctYYYYMMDDxzy.

    Replace YYYYMMDD with today's date.

    Replace xyz with your initials.

    Note: Storage account names must be unique to the world, must be between 3-24 characters, and must be only lowercase characters and numbers.

    resource "azurerm_storage_account" "iac_stg_acct" {
      name                     = "iacstgacctYYYYMMDDxzy"
      resource_group_name      = "{YOUR RESOURCE GROUP NAME}"
      location                 = "{YOUR RESOURCE GROUP LOCATION}"
      account_tier             = "Standard"
      account_replication_type = "LRS"
    }
    

    Note: The resource group name and location must match the values you provided here.

Step 3 Completion Check

Before moving on, make sure you have the file in place for the storage account creation.

"Terraform ready with storage account"

Task 2 - Run the deployment

As mentioned in part 1, there are 3 commands that make up the basic Terraform workflow:

  • terraform init
  • terraform plan
  • terraform apply

The first command only needs to be executed when creating a new configuration or updating an existing one. However, running the command multiple times should not cause any issues.

Step 1 - Issue commands to run the deployment

In this step, you'll deploy the storage account using Terraform.

  1. Ensure you are in the correct directory

    Before you execute any commands, make sure that you are in the iac\terraform\Part1_Main_Modulefolder

    "Terraform directory."

  2. Terraform Init Command [terraform init]

    Execute the terraform init command.

    terraform init

    After the command completes you should see the following:

    "Terraform init results."

  3. Terraform Plan Command [terraform plan]

    Execute the terraform plan command:

    terraform plan -out main.tfplan
    

    Note: the command creates a main.tfplan file.

    You should see the following output (some details are omitted):

    "Terraform init results."

    "Terraform init results."

  4. Terraform Apply Command [terraform apply]

    Finally, apply the plan by executing the following command:

    terraform apply main.tfplan
    

    You should see the following output:

    "Terraform apply results."

Step 2 - Verify the deployment

Before moving on, ensure that you have a storage account in your resource group with a name that matches the value provided in the main.tf file.

"Storage Account exists as expected"

Task 3 - Create providers file

As mentioned in part 1, when working with Terraform it is recommended to create separate files to keep everything organized. In this step, you will create a separate providers.tf file.

Step 1 - Create providers.tf file

For the first step, you'll just create a file.

  1. Create a new file in the iac\terraform\Part1_Main_Module folder

    providers.tf
    
  2. Move terraform and providers blocks to providers.tf file

    Cut the terraform and providers blocks from the main.tf file, and past to the providers.tf file, this should have no impact on your deployment.

    "Providers and Main after changes side by side"

Step 2 - Confirm there has been no impact on your deployment

As moving the location of the terraform and providers section should have no impact, it's important to verify this is the case.

  1. Confirm there is no impact on your deployment

    To confirm execute the terraform plan command,

    terraform plan -out main.tfplan

    You should see the following messages:

    No changes. Your infrastructure matches the configuration.
    
    Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
    

Task 4 - Use input variables

In Terraform module parameters are referred to as input variables or simply variables, in this part of the workshop you'll create input variables for the storage account name and location. You'll also learn how to use the variables in your deployment.

Step 1 - Add input variables to the terraform file

For starters, we will only add input variables for the resource group name, storage account name and location of the storage account.

  1. Add variables for resource group name, storage account name, and location.

    Add the following code to the top of the main.tf file:

    variable "resourceGroupName" {
        type = string
        nullable = false
        default = "{YOUR RESOURCE GROUP NAME}"
    }
    
    variable "storageAccountName" {
        type = string
        nullable = false
        default = "{YOUR STORAGE ACCOUNT NAME}"
    }
    
    variable "location" {
        type = string
        nullable = false
        default = "{YOUR RESOURCE GROUP LOCATION}"
    }
    
  2. Leverage the variables with var.<variablename> in the main.tf file

    Use the variables to populate the storage account values. Terraform references input values using the var object. Modify the storage account resource block should to use the variables created above as follows:

    resource "azurerm_storage_account" "iac_stg_acct" {
      name                     = var.storageAccountName
      resource_group_name      = var.resourceGroupName
      location                 = var.location
      account_tier             = "Standard"
      account_replication_type = "LRS"
    }
    

    "the current main.tf with variables in place"

  3. Validate everything is still the same on the plan and state

    Execute the terraform plan command again, since there were no infrastructure changes you should see this message again:

    No changes. Your infrastructure matches the configuration.
    
    Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
    

Step 2 - Create a variables file

In the previous step you added the ability to use input variables (or parameters) in the terraform template, in this step you will continue with the best practices mentioned above by moving those variable definitions to a separate file.

  1. Create a variables.tf file for variables

    Add a new file variables.tf to your working directory

    touch variables.tf

    please note: this filename is just a suggested name and that as long as a file is in the same directory, Terraform will automatically make those variables available in the main module.

  2. Put the variables into the variables.tf file

    Cut the variables from the main.tf file and paste the variable declarations from the to the variables.tf file, after the change, the only thing left in the main.tf file should once again be your resource block.

    "Variables are in their own file shown side-by-side with main"

Step 3 - Validate No changes with variables file in place

Validate there are still no changes even with the new file structure

  1. Execute the terraform plan command again

    Since nothing has changed to update the infrastructure you should again see no changes in the plan.

Step 4 - Deployments without default/provided values

You might have noticed that so far you haven't been asked to provide a value for the variables during deployment. This is because of the default values that have been put in place in the files.

In this step, you'll see what happens if you try to do a deployment without default/provided values.

  1. Remove the default value for the resourceGroupName variable.

    In the variables.tf file, delete the line default = "{YOUR RESOURCE GROUP NAME}", but leave everything else in place.

    "no default value for resource group in variables file"

  2. Execute the terraform plan command

    With the change, run the plan again:

    terraform plan -out main.tfplan

    you should see this prompt:

    "Terraform variable prompt."

    This is obviously not the most efficient deployment strategy and is also error prone, we will look at a better deployment option in the next step.

    Enter your resource group name to save the plan.

    "no changes when the same name is entered"

Step 5 - Use a variable definitions file

In this step, you will use a special file called a variable definitions file to specify the values you want to use in the deployment.

  1. Add a file called terraform.tfvars to your working directory.

    Create the file:

    touch terraform.tfvars

    Terraform automatically scans for this specific file when deploying resources.

    Note: If you want to use a different file name you would need to also use the -var-file parameter when executing the plan command.

  2. Move variables to the new file

    Type the name of any of the variables declared in the variables file, if you are using the Terraform extension for Visual studio code you should see something like this:

    "Terraform apply results."

    Provide a value for all the variables.

    "tfvars file completed"

  3. Execute the terraform plan command again.

    With all of the variables defined and then given a value, you have seen how to separate the variable definitions from the values for powerful template deployment configurations.

    To be clear, you should not be prompted to provide a value for and variables and you should still see no changes to your deployment in the plan since no infrastructure changes are taking place.

Completion Check

You now have a file that you can reuse in multiple resource groups with various storage account names (you would need to change the name in the parameter file at this point to ensure it is unique).

Task 5 - Use data sources

Up until now, you have used variables to provide the name and location of the resource group that contains the storage account. However, Terraform has another way to access information defined outside of Terraform or that is part of a different deployment using data sources.

In this step, you will modify the files you currently have to use a data source to access the resource group information instead of providing the values through variables.

Step 1 - Add data source to configuration

In this first step, you'll add a data source for the resource group

  1. Add the data_rg resource group declaration

    Go to the top of the main.tf file and type da, if you are using the Terraform extension for VS code you should see the following:

    "Terraform data block autocomplete."

    Hit the tab key, a data block will be created automatically:

    "Terraform data block options."

  2. Change the type to azurerm_resource_group

    Type azurerm_resource_group,

    azurerm_resource_group

    At this point, autocomplete should display the option after you type a few characters:

    "Terraform data block resource group."

  3. Set the name of the resource group and location for the resource group

    The resource group data source requires the name of the resource group. You can use the resourceGroupName variable to populate the parameter on this data source. The new data block should look like this:

    data "azurerm_resource_group" "data_rg" {
      name = var.resourceGroupName
    } 
    

Step 2 - Use data source values in storage account configuration

In this step, you'll update the declaration in the storage account to leverage the resource group data source.

  1. Replace the resource_group_name and location

    You can now replace the resource_group_name and location parameters in the storage account block with the values from the data source using the following syntax:

    data.{RESOURCE_TYPE}.{DATA_SOURCE_NAME}.{DATA_SOURCE_PROPERTY}
    

    For example, to access the name of a resource group with a data source:

    data.azurerm_resource_group.data_rg.name
    

    and

    data.azurerm_resource_group.data_rg.location
    
  2. Validate the file text before proceeding

    After replacing the name and location of the resource group the storage account block should look something like this:

    resource "azurerm_storage_account" "cm_stg_acct" {
      name                     = var.storageAccountName
      resource_group_name      = data.azurerm_resource_group.data_rg.name
      location                 = data.azurerm_resource_group.data_rg.location
      account_tier             = "Standard"
      account_replication_type = "LRS"
    }
    

Step 3 - Remove location variable and execute deployment.

In this step you'll remove the location variable and execute the deployment

  1. Remove the location variable

    Since we are now getting the resource group information from the data source we can now remove the location variable from the variables.tf and the value assignment from the terraform.tfvars file.

    Note: the state of the system already knows the resource group exists. In this template, the resource group would not be created since no location is provided if there were not currently a state that holds the value of the current existing resource group.

    "no location variable remains"

  2. Execute the plan and validate there are no changes

    You can now execute the terraform plan command again, since we are not adding or removing any resources you should see a message saying that no changes were detected.

Task 6 - Use local variables and functions

In this module you will learn to use local variables and functions to create a unique string name for the storage account name. The term local variable in terraform refers to any variable used inside a module.

Step 1 - Add a unique identifier input variable to the storage account

Since the storage account name needs to be unique across all resources in Azure you will now add a unique identifier section to the storage account name to comply with this requirement.

  1. Add a uniqueIdentifier variable to your variables file.

    In the variables.tf file, add a new variable as follows:

    variable "uniqueIdentifier" {
        type = string
        nullable = false
        default = "20291231acw"
    }
    
  2. Add the value in the terraform.tfvars file.

    Based on the value you used earlier, add the variable in the terraform.tfvars file with the following format: YYYYMMDDabc. However, for this exercise you want a second storage account so change the date by one day to make it unique from the other storage account.

    For example, if the value was 20291231acw for the first one, creat the second one as follows:

    uniqueIdentifier: 20291230acw
    
  3. Repeat the previous two steps to create the base storage account name as a varaiable.

    In the variables.tf file, add the following code:

    variable "storageAccountBase" { 
        type = string 
        nullable = false 
        default = "iacstgacct" 
    } 
    

    Then add the value to the terraform.tfvars file:

    storageAccountBase = "iacstgacct"  
    

Step 2 - Add a local variable for the full storage account name

Local variables in Terraform are declared using a locals block. In this step, you'll add a local variable for the full name of the storage account.

  1. Add a locals block to your main.tf file

    At the top of the main.tf file, add a variable called storageAccountNameFull. Assign a value by concatenating the storageAccountName and uniqueIdentifier variables using interpolation:

    locals {
        storageAccountNameFull = "${var.storageAccountBase}${var.uniqueIdentifier}"
    }
    
  2. Define the second storage account

    Add another storage account resource block by copying the existing one and replacing the name parameter with the local variable you just created, the following syntax is used to access local variables:

    local.{YOUR VARIABLE NAME}
    

    So the overall block should be similar to this (Note the resource declaration uses iac_stg_acct_full here as well):

    resource "azurerm_storage_account" "iac_stg_acct_full" { 
      name                     = local.storageAccountNameFull
      resource_group_name      = data.azurerm_resource_group.data_rg.name
      location                 = data.azurerm_resource_group.data_rg.location
      account_tier             = "Standard" 
      account_replication_type = "LRS" 
    }
    
  3. Execute the plan to validate you will be creating a new storage account

Execute the terraform plan command again, you should see a message saying that 1 resource will be added.

terraform plan -out main.tfplan
  1. Execute the apply command to create the storage account

Execute the terraform apply command

terraform apply main.tfplan

Once the command is completed you should see the new storage account in your resource group.

"Two storage accounts"

Step 3 - Use a provider to add a unique string to the storage account name

If you completed the Bicep section of this workshop, you will recall that the uniquestring function allows you to generate a string that can help make resource names unique.

In this step, you will use a provider to access create a unique variable string in Terraform.

  1. Create a random object

    Add the following after the azurerm element in the required providers portion of the providers.tf file:

    random = {
      source = "hashicorp/random"
      version = "3.6.2"
    }
    

    Reminder: if at any point you get lost, don't forget to check the solution files for help in locating where something should go

  2. Modify the main.tf file to generate a unique string

    Generate a unique id by adding the following block to your main.tf file:

    resource "random_string" "random" {
      length           = 10
      special          = false
      lower            = true
      upper            = false 
    }
    

    Note: The name for storage accounts does not allow any special characters so we are forcing the generated string to comply to this. You can check the documentation for the random provider for additional configuration options.

  3. Add a new local variable called storageAccountNameUnique

    In the main.tf file, in the locals block, under the current storageAccountNameFull, add a new local variable storageAccountNameUnique, and assign the following value:

    storageAccountNameUnique = "${var.storageAccountName}${var.uniqueIdentifier}${random_string.random.result}"
    
  4. Create the resource for a third storage account.

    Add a new storage account resource block and assign the name using the new variable created. Use the new local variable for the storage account name.

    resource "azurerm_storage_account" "iac_stg_acct_unique" { 
      name                     = local.storageAccountNameUnique
      resource_group_name      = data.azurerm_resource_group.data_rg.name
      location                 = data.azurerm_resource_group.data_rg.location
      account_tier             = "Standard" 
      account_replication_type = "LRS" 
    }
    
  5. Re-initialize Terraform with the terraform init command

    Since we added a provider to our deployment, we need to run the terraform init command before we can create a new plan, otherwise you'll see the following message:

    "Terraform provider error."

    Make sure all your files are saved, then run the command:

    terraform init
  6. Deploy using the plan and apply commands.

    Deploy the new resource by executing the plan and apply command, you should see 2 resources being created in the plan: one for the uniqueid and one for the new storage account.

    terraform plan -out main.tfplan
    terraform apploy main.tfplan

    Important: - This deployment will fail (and that's expected)

    Your deployment should fail due to the length of the new storage account name being over 24 characters, in the next step we will use one of the built-in functions in Terraform to solve the problem.

    "The deployment fails because the name is too long"

Step 4 - Use a function to truncate the resource name

Terraform has multiple built-in functions that can be used to transform and combine values, you can find a full list here: https://developer.hashicorp.com/terraform/language/functions.

In this step, you will use the substr function to truncate the length of the storage account name:

  1. Leverage the substr function

The substring function has the following format:

substr(string, offset, length)
  1. Use the substr function to ensure the value of the storageAccountNameFullUnique variable is a maximum of 24 characters.

    Change the declaration in the locals section of main.tf to the following for the storageAccountNameUnique:

    storageAccountNameUnique = substr("${var.storageAccountName}${var.uniqueIdentifier}${random_string.random.id}",0,24)
    
  2. Run the plan and apply commands again as above.

    Run the commands:

    terraform plan -out main.tfplan
    terraform apploy main.tfplan

    This time, you should see your storage account created as expected in the portal:

    "Random string works to generate teh new file"

Step 5 - Use a validator to ensure the storage account name is unique and long enough

Input variables allow your Terraform files to be dynamic but without any validations there is also the risk that the values provided will result in invalid properties for your resources. For this reason, Terraform provides different ways to validate your configuration.

In this step, you will add validations to a couple of input variables to prevent any issues with planning and/or deployment.

Variable validations are added using the validation property of a variable.

For example:

variable "image_id" {
  type        = string
  description = "The id of the machine image (AMI) to use for the server."

  validation {
    condition     = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
    error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
  }
}
  1. Validate the storageAccountName variable

    Add the following validation block to the storageAccountName variable in the variables.tf file:

    validation {
      condition = length(var.storageAccountName) > 3
      error_message = "The storage account name should be at least 3 characters"
    }
    

    For a resulting block of:

    variable "storageAccountName" { 
        type = string 
        nullable = false 
        default = "iacstgacct20291231acw" 
    
        validation {
          condition = length(var.storageAccountName) > 3
          error_message = "The storage account name should be at least 3 characters"
        }
    }  
    
  2. Prove that the validation is working as expected.

    In the terraform.tfvars file, assign the value c to the storageAccountName variable.

    Try to execute the plan.

    terraform plan -out main.tfplan

    You should see the following message:

    "storageAccountName validation error."

  3. If time permits, add 2 more validations:

    • Validate that the uniqueIdentifier variable is 11 characters long.
    • Add an environment input variable that will allow only two (2) values (hint: look at the contains function): dev and prod and use that to create a new storage account resource.

    Reminder: You can also review the solution files for additional help.

Completion Check

You can now deploy the same file to different resource groups multiple times and it will create a unique storage account name per resource group (and per environment if needed) using local variables, built-in functions and validations.

If time permits, you can try creating a different resource group and use what you have built so far to deploy these resources there.

Task 7 - Use Modules for resource deployments

So far, we have been working completely out of our main module. In application deployments like the one you will do in part 2, you will want to be able to have your resources distributed in modules.

In this task, you'll learn about modules using a similar deployment to the current deployment.

Step 1 - Create a resource group

In your previous deployment you were using information from an existing resource group, however, in a real world scenario you might have to create the entire infrastructure including the resource group.

In this step, you will create a new template that deploys a new resource group.

Now it's time to see how much you can remember from above!

  1. Create a new folder for this second deployment

    Create a new folder under the terraform folder called Part1_Modules and add the following files:

    • providers.tf
    • variables.tf
    • main.tf
    • terraform.tfvars
  2. Leverage what you know to create a valid providers.tf file

    Add the following providers to the providers.tf file:

    • azurerm
    • random
  3. Create variables for resourceGroupName and location

    Add the following variables to variables.tf:

    • resourceGroupName: string type, not nullable.
    • location: string type, not nullable, validation to only allow East US as a value (optional).
  4. Leverage the terraform.tfvars file to create values for the variables

    Assign values to the variables in the terraform.tfvars file.

    • resourceGroupName: iac-training-rg-modules
    • location: eastus
  5. Use the azurerm_resource_group to define the group in main.tf

    Add a resource group block to the main.tf file, the resource type needed is azurerm_resource_group. Leverage the variables for name and location created above.

  6. Deploy the resource group

    Run the commands to get your deployment into the cloud.

    Hint: Don't forget to initialize Terraform in the new directory!

    terraform init
    

    "Two Groups"

    Reminder: If at any point you get lost, take a look at the files in the solutions folder to see if you can get unstuck.

Step 2 - Create a module for the storage account

Now that you have a new resource group, your next task is to create a module for the storage account.

In Terraform, you can create your own modules locally or you can get modules from the Terraform registry, you can even publish your own modules for other people to use.

For this exercise, you will focus on local modules.

  1. Add a modules folder in your working directory and add a storageAccount inside of it.

    In the current folder where you started your new resource group, add the subdirectory storageAccount

  2. Create main.tf and variables.tf files in the storageAccount folder.

    Add variables for the storage account name composition with base, uniqueIdentifier, and environment storageAccountNameFull:

    storageAccountNameFull
    

    Hint: The local variables.tf file for the nested storage account should only need three variables defined.

  3. Create a new storage account definiation in the main.tf file in the storageAccount folder using terraform

    Leverage any of the storage account resource blocks and learning you've done from the root module created in the previous exercise and replace the values assigned to the name, resource_group_name and location with variables.

    Hint": Use the following variable names, and make sure they are defined as variables for use in the terraform:

    • storageAccountNameFull (composed from base + uniqueIdentifier + environment)
    • resourceGroupName
    • location

    Additional Hint: The storageAccountNameFull variable will be passed in so it should not have a default value.

  4. Where is the terraform.tfvars file?

    You will only need the top-level terraform.tfvars file, there is no need to nest one in the storageAccount folder. This allows you to share the variables among all modules, defined in only one place with values.

  5. Add the variables specified or necessary to create the variables specified in the previous step to the top-level variables.tf file.

    Remember, you will be composing the storageAccountNameFull variable, so that should not be defined in the variables.tf file (what variables do you need to define?).

    Hint: There are three variables needed to define the full storage account name that will also be combined with a random string.

    With that in mind, there should be five variables defined in the variables.tf file. What do you think they are?

    Take a minute to think about this. If you get stuck, you can always refer to the solution files.

  6. Define the values for the variables in the terraform.tfvars file

    You will need to make sure the defaults are set for all the variables.

    Hint: Since the name of the storage account is confined and we want it to be a combination of base + unique + env + random, and we know unique is 11 chars and env is either 3 or 4 chars, keep your base storage account to greater than 3 and less than 8 chars or there will be no room for the random string to append any chars to the end.

    Suggestion, use something like this:

    storageAccountNameBase: iacstg
    

    Final Hint: Your terraform.tfvars should define values for the five variables, none of which should be named storageAccountNameFull. Where do you think you need to define that variable?

  7. Add the new storage account as a module.

    Add a reference to the storage account module by adding the following block in the top-level main.tf file, under the declaration for the resource group:

    module "storageAccount" {
      source = "./storageAccount"
    
      storageAccountNameEnv = local.storageAccountNameFull
      resourceGroupName     = var.resourceGroupName
      location              = var.location
    }
    

    The main parameter you need to supply when using modules is the source directory where the module's main.tf file is defined (in this case, it should just be nested in storageAccount).

    For local modules the source value should be populated with the ./<folderpath> notation.

    Hint: The module will be created when you initialize terraform

    You also need to pass values for any variables that the module is expecting from the top-level. In this case, you'll need to compose the full storage account name in a local variable for the main.tf file.

    Hint: This was done in the previous sections using a locals section. Make sure to include the environent as part of the name composition!

  8. Deploy the solution

    As mentioned previously, using the module requires a quick compilation. For that reason, what should you do first when getting ready to deploy?

    Hint: The answer is not plan or apply

    If you run the wrong command here, you'll see an error:

    "Error is shown when the module has not been initialized"

    Run the commands to initialize, plan, and apply the changes. When this is done, you should have a new storage account that follows the pattern base + uniqueIdentifer + environment + randomstring.

  9. Validate the solution

    Make sure the new resource has been successfully deployed before moving on.

    "Initial module deployment is completed"

Task 8 - Use outputs from modules

There is one more Terraform element that you have not worked with yet: outputs.

When creating resources, some of the properties of those resources are exported so you can use them as parameters for other steps in your deployment. In this step, you will export some of the storage account properties as outputs to be leveraged in other modules.

In this task, you'll learn how to use outputs from the storageAccount module to get properties of the storage account to another module for creating a storage account container.

Note: Yes, in the real world you could create them together in the same file. Please note that for academic reasons, separation of the container to a new file is used here for demonstration purposes.

Step 1 - Create an output for the storage account

In this step you'll create an outputs.tf file and use that to get values from the storageAccount module.

  1. Create an outputs.tf file in the storage account module directory.

    Navigate to the storage account module directory and create a new file

    touch outputs.tf
  2. Export the storage account properties name, id, and location for use in other modules.

    Add 3 output blocks to the newly created file: account name, id and location. The following is an example of the output block for the account name:

    output "{OUTPUT_VARIABLE_NAME}" {
      value = azurerm_storage_account.{YOUR_STORAGE_ACCOUNT_RESOURCE}.name
    }
    

    For example:

    output "storageAccountName" { 
      value = azurerm_storage_account.iac_stg_acct_module.name 
    } 
    

    Repeat the outputs for the remaining account properties id and location.

  3. Validate that nothing has changed in the plan.

    Run the terraform plan command to validate there are currently no changes to apply.

    Since we are not updating the infrastructure executing the plan and apply commands will have no effect, we will do that in the next step.

Step 2 - Create a module that needs outputs from another module

With the outputs generated by the storage account resource, you can now leverage the values to create a storage container resource.

Reminder: This is an academic exercise and in the real world you would likely create the storage account container along with the storage account in the same module.

  1. Create a new folder inside your modules folder called storageContainer.

    At the same level as the storageAccount folder, create a new folder

    mkdir storageContainer
  2. Add a main.tf and a variables.tf file.

    Just as with the storage account creation, you'll need the two files for module and variable declarations:

    cd storageContainer
    touch main.tf
    touch variables.tf
  3. Add 2 variables: storageAccountName and containerName.

    In the variables.tf file, add the two variables:

    variable "storageAccountName" {
        type = string
        nullable = false
    }
    
    variable "containerName" {
        type = string
        nullable = false
    }
    
  4. Add an azurerm_storage_container resource block in the main.tf file.

    In the main.tf file, add a new resource

    azurerm_storage_container
    

    Populate the container properties with the variables you created. Make sure to name the resource something useful like images_container or whatever you think you might name your container.

    For example:

    resource "azurerm_storage_container" "images_container" {
        name                  = var.containerName
        storage_account_name  = var.storageAccountName
    }
    

    Reminder: if you get lost, leverage the solution files.

  5. Modify the root level variables to contain a variable for the new containerName.

    Add a containerName variable to the input variables of the root folder's variables.tf file:

    variable "containerName" {
        type = string
        nullable = false
    }
    ``  
    
    
  6. Add a default value to the terraform.tfvars file in the root folder

    In the root folder, add a default name images for the container to the terraform.tfvars file:

    containerName = "images"
    
  7. Modify the main.tf file in the root to add the deployment of the storageContainer module.

    Add a storageContainer module block to the root module and populate the storageAccountName with the output value from the storageAccount module:

    module "storageContainer" {
        source = "./storageContainer"
        containerName      = var.containerName
        storageAccountName = module.storageAccount.storageAccountName
    }
    
  8. No deployment yet by design.

    There is one final (optional) thing to do to get the deployment to work more professionally, which you will do in the next and final task.

Task 9 - Use azurerm backend

As you already know, the state file is key for Terraform to know what changes need to be done to the infrastructure. This state file can be saved in different locations.

Up to this point, we have been using the local backend, but in this step we will add a remote backend which will be needed for automated deployments.

Additionally, this will make sure that the state is not only saved on one developer's machine.

Step 1 - Create infrastructure for state file

To make this easy, you'll leverage an Azure storage account for your state files. You will need to make sure you understand this part well, because it will be critical to the next part where you will do a full resource deployment to Azure.

Important: This storage account and container name are new and are not any that you've previously created, nor should you try to create this with terraform at this time. You only need this once and in the interest of time you should just manually provision the account and container with new values in a new resource group.

Go to the Azure Portal and create the following resources (remember to replace the YYYYMMDDxxx with the date and your initials):

  • RG: rg-terraform-github-actions-state
  • Storage Account: tfghactionsYYYYMMDDxxx
  • container: tfstatepart1
  • performance: standard
  • redundancy: LRS

Note: Each deployment state will leverage a unique container. This will allow you to reuse this same storage account in the next parts of this workshop for automated deployments.

"Storage account with container for state file"

Step 2 - Copy your local state file to storage container

In this step, you'll upload your current terraform.state file to the storage container.

  1. Open the container at Azure

    In the Azure portal, open the container, then select Upload to upload the file

    "Upload your state file"

    Important: After upload, you should see it in the container at Azure:

    "Remote state."

    Hint: you'll need to browse to the folder where you were running the apply commands earlier.

Step 3 - Add a backend block to the providers.tf file

In this step, you'll tell your local deployment to use the Azure storage account when interactions with the state file need to happen:

  1. Modify the providers.tf file in the root folder to add the provider.

    Open the providers.tf file and add this block after the required_providers section of the main terraform declaration:

    backend "azurerm" {
        resource_group_name  = "rg-terraform-github-actions-state"
        storage_account_name = "{YOUR_STORAGE_ACCOUNT_NAME}"
        container_name       = "tfstatepart1"
        key                  = "terraform.tfstate"
        use_oidc             = true
    }

    Note: Make sure to put the correct storage_account_name and container_name in your backend provider declaration.

Step 4 - Create a deployment plan

To complete this first part, it's time to do the final deployment and leverage the state file in the azure storage account.

  1. Reinitialize Terraform

    With the backend configuration set to Azure, you need to execute the terraform init command

    terraform init
  2. Run the plan

    Execute the terraform plan command

    terraform plan -out main.tfplan

    Since we never created the container above, you should see the changes for that showing in the plan now.

  3. Deploy

    Deploy the terraform apply command

    terraform apply main.tfplan
  4. Validate your storage container is in place and everything is now working to complete this module

    You should now be using the state file in the Azure storage account and you will also have created the storage account with container as expected from the module that leveraged the outputs above.

    "final validation is working as expected"

Completion check

CONGRATULATIONS!!!!

At this point you have learned most of the basic concepts you will need to work with Terraform for infrastructure deployments.

Make sure that all the files were created successfully and that you can re-run your deployments at will before proceeding.

The repetitive and consistent nature of the deployments are some of the main reasons you want to use Infrastructure as Code.

Conclusion

In this first part of the workshop, you learned how to work with Terraform to create storage account resources in a resource group. Along the way you learned the following concepts:

  • creating terraform files
  • running deployments from the command line
  • using input and local variables
  • using data sources
  • using functions
  • using modules
  • using outputs

Final Thoughts for part one and next steps

Remember that you can review the solution files to validate your solution looks mostly similar.

You are now ready to move on to the next part:

Part2-BuildingTheInfrastructure

Make sure to take care of the common tasks first, then after completing the common tasks, move to the Terraform specific walkthrough for part 2.