{"id":1239,"date":"2026-10-14T13:49:46","date_gmt":"2026-10-14T13:49:46","guid":{"rendered":"https:\/\/www.devopsschool.com\/tutorials\/?p=1239"},"modified":"2026-10-14T13:49:48","modified_gmt":"2026-10-14T13:49:48","slug":"terraform-lab-azure-hands-on-terraform-functions","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/tutorials\/terraform-lab-azure-hands-on-terraform-functions\/","title":{"rendered":"Terraform Lab \u2013 Azure Hands-On \u2013\u00a0Terraform Functions"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Beginner to Intermediate \u00b7 8 modules \u00b7 22 progressively designed labs \u00b7 20 local-only labs + 2 optional cloud labs \u00b7 Updated 14 October 2026<\/strong><\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Purpose:<\/strong>&nbsp;learn Terraform functions, not another infrastructure service. Every lab has short, complete code and a practice task. Azure Linux VM is used only near the end; AWS uses one VPC and one subnet. No resource is created until a learner deliberately runs an optional cloud lab.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Previous-course compatibility:<\/strong>&nbsp;The accompanying Azure variables lab used Terraform&nbsp;<strong>1.15.9<\/strong>, AzureRM&nbsp;<strong>5.9.x<\/strong>, an Azure VNet and a Linux\/Windows VM. This course retains that baseline where Azure is deployed, while keeping all the local-only lessons independent.&nbsp;<strong>Never overwrite or switch state files between this package and your old 13-resource Azure lab.<\/strong><\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">Learning objectives<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Students will learn to evaluate functions in&nbsp;<code>terraform console<\/code>, manipulate strings\/maps\/lists\/numbers, guard optional values, render files and cloud-init, handle path\/time functions correctly, derive CIDR networks, understand deterministic versus non-deterministic results, and combine functions in safe Azure and AWS labs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Progression at a glance<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>flowchart LR\n    A&#91;Input values] --&gt; B&#91;Functions and expressions]\n    B --&gt; C&#91;Local values and outputs]\n    C --&gt; D{Deployment needed?}\n    D --&gt;|No| E&#91;Console or plan]\n    D --&gt;|Yes| F&#91;Review cloud plan]\n    F --&gt; G&#91;Apply then verify]\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Module and lab plan<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Module<\/th><th class=\"has-text-align-left\" data-align=\"left\">Labs<\/th><th class=\"has-text-align-left\" data-align=\"left\">Topic<\/th><th class=\"has-text-align-left\" data-align=\"left\">Cloud required?<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td>01\u201302<\/td><td>Introduction to Terraform Functions<\/td><td>No<\/td><\/tr><tr><td>2<\/td><td>03\u201305<\/td><td>String Functions<\/td><td>No<\/td><\/tr><tr><td>3<\/td><td>06\u201308<\/td><td>Collection Functions<\/td><td>No<\/td><\/tr><tr><td>4<\/td><td>09\u201310<\/td><td>Numeric and Mathematical Functions<\/td><td>No<\/td><\/tr><tr><td>5<\/td><td>11\u201312<\/td><td>Type Conversion, Validation, and Error Handling<\/td><td>No<\/td><\/tr><tr><td>6<\/td><td>13\u201315<\/td><td>File, Encoding, and Data Functions<\/td><td>No<\/td><\/tr><tr><td>7<\/td><td>16\u201317<\/td><td>Filesystem, Path, and Time Functions<\/td><td>No<\/td><\/tr><tr><td>8<\/td><td>18\u201322<\/td><td>Advanced Functions and Real-World Use Cases<\/td><td>Only for optional labs 21\u201322<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Tools and prerequisites<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Terraform CLI<\/strong>: training baseline\u00a0<code>1.15.9<\/code>\u00a0(the exercises use\u00a0<code>>= 1.15.0, &lt; 2.0.0<\/code>). See\u00a0<a href=\"https:\/\/developer.hashicorp.com\/terraform\/install\">Install Terraform<\/a>.<\/li>\n\n\n\n<li>Text editor and terminal (macOS, Linux, Windows\/WSL or PowerShell).<\/li>\n\n\n\n<li><strong>Labs 01\u201320:<\/strong>\u00a0no cloud account, no Azure CLI, no AWS CLI, and no Terraform provider download required\u00a0<strong>except<\/strong>\u00a0Lab 19, which references Terraform&#8217;s bundled built-in\u00a0<code>terraform<\/code>\u00a0provider.<\/li>\n\n\n\n<li><strong>Lab 21 only:<\/strong>\u00a0Azure CLI\/credentials, an Azure subscription and quota, a supported Linux VM image\/SKU, an existing\u00a0<strong>public<\/strong>\u00a0SSH key; no password or public IP required.<\/li>\n\n\n\n<li><strong>Lab 22 only:<\/strong>\u00a0configured AWS identity with EC2 network creation permissions.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Standard hands-on workflow (for a local lab)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">From the extracted package root, replace&nbsp;<code>&lt;LAB_FOLDER&gt;<\/code>&nbsp;with a directory inside&nbsp;<code>labs\/<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -version\nterraform -chdir=labs\/&lt;LAB_FOLDER&gt; init -backend=false\nterraform -chdir=labs\/&lt;LAB_FOLDER&gt; fmt\nterraform -chdir=labs\/&lt;LAB_FOLDER&gt; validate\nterraform -chdir=labs\/&lt;LAB_FOLDER&gt; plan -input=false\nterraform -chdir=labs\/&lt;LAB_FOLDER&gt; apply -auto-approve\nterraform -chdir=labs\/&lt;LAB_FOLDER&gt; output\nterraform -chdir=labs\/&lt;LAB_FOLDER&gt; console\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You can type an expression into the&nbsp;<code>console<\/code>, e.g.&nbsp;<code>lower(\"PROD\")<\/code>, and type&nbsp;<code>exit<\/code>&nbsp;when finished. The&nbsp;<code>apply -auto-approve<\/code>&nbsp;shortcut is&nbsp;<strong>only for the local, no-resource lessons<\/strong>. Do not copy it into Labs 21\u201322. Without&nbsp;<code>apply<\/code>, outputs can often be examined in&nbsp;<code>plan<\/code>, but&nbsp;<code>terraform output<\/code>&nbsp;reads the latest&nbsp;<strong>state<\/strong>, not the current configuration by itself. Local plans do not provision cloud resources.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Independent folders are intentional:<\/strong>&nbsp;each lab evolves the running&nbsp;<em>concept<\/em>&nbsp;without requiring the learner to reuse the same Terraform state; separate directories prevent accidental deletions or changes to the previous labs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Useful commands for instructors<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code><em># Example: Lab 04 (local-only)<\/em>\nterraform -chdir=labs\/lab-04-string-transform-and-split init -backend=false\nterraform -chdir=labs\/lab-04-string-transform-and-split validate\nterraform -chdir=labs\/lab-04-string-transform-and-split plan\n\n<em># Run local syntax\/init\/validate checks (requires installed terraform)<\/em>\nbash tools\/validate_labs.sh\n\n<em># Optional: check cloud labs' provider configuration too; downloads providers<\/em>\nbash tools\/validate_labs.sh --include-cloud\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Source-file conventions<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>main.tf<\/code>: standalone minimal lab, usually only\u00a0<code>locals<\/code>\u00a0and\u00a0<code>output<\/code>.<\/li>\n\n\n\n<li><code>files\/<\/code>: tiny static JSON\/YAML\/text fixtures, never secrets.<\/li>\n\n\n\n<li><code>cloud-init.tftpl<\/code>: small template introduced in Lab 15 and reused in the Azure VM lab.<\/li>\n\n\n\n<li><code>README.md<\/code>: short runbook beside each lab.<\/li>\n\n\n\n<li>Generated\u00a0<code>.terraform\/<\/code>, state, credentials and saved plans are ignored by\u00a0<code>.gitignore<\/code>.\u00a0<strong>Commit\u00a0<code>.terraform.lock.hcl<\/code><\/strong>\u00a0in real infrastructure repositories to retain provider dependency selections.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Important security and state rule<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Your uploaded&nbsp;<strong>Azure Part 2<\/strong>&nbsp;lab is a&nbsp;<strong>13-resource existing-state exercise<\/strong>, covering Windows and Ubuntu VMs. This course is separate. The Azure deployment example intentionally uses a&nbsp;<em>different, five-resource environment<\/em>&nbsp;and SSH keys instead of copied passwords. Do&nbsp;<strong>not<\/strong>&nbsp;place the new&nbsp;<code>main.tf<\/code>&nbsp;beside your existing&nbsp;<code>terraform.tfstate<\/code>. If applying a derived expression to your previous deployment, use a new&nbsp;<code>locals<\/code>\/<code>output<\/code>&nbsp;only, then review a no-replacement plan. See&nbsp;<code>REFERENCE_AZURE_LAB_SAFE_INTEGRATION.md<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Module 1 \u2014 Introduction to Terraform Functions<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Goal:<\/strong>&nbsp;Build confidence with function calls, variables, expressions, and terraform console.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Terraform includes built-in functions; you call them inside an expression like&nbsp;<code>lower(\"DEV\")<\/code>&nbsp;or&nbsp;<code>max(1, 2)<\/code>. Functions do not themselves create resources and you cannot define your own general-purpose HCL functions. The output is a value, often used in&nbsp;<code>locals<\/code>, an&nbsp;<code>output<\/code>, a variable validation rule, or an attribute of a resource.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Variable:<\/strong>\u00a0input provided by configuration,\u00a0<code>-var<\/code>, environment, or a\u00a0<code>.tfvars<\/code>\u00a0file, referenced using\u00a0<code>var.environment<\/code>.<\/li>\n\n\n\n<li><strong>Expression:<\/strong>\u00a0a combination of values, references, operators, and calls, e.g.\u00a0<code>\"vm-${lower(var.environment)}\"<\/code>.<\/li>\n\n\n\n<li><strong>Function:<\/strong>\u00a0the operation inside an expression, e.g.\u00a0<code>lower(var.environment)<\/code>.<\/li>\n\n\n\n<li><strong>Local value:<\/strong>\u00a0reusable named expression, e.g.\u00a0<code>local.resource_name<\/code>.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Most pure functions can be evaluated as part of planning; some exceptions are non-deterministic functions and plan-context functions. The console is an excellent way to experiment without deploying anything.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 01 \u2014 Your first functions in terraform console<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-01-console-first-functions\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>lower(), upper(), length()<\/code><br><strong>Objective:<\/strong>&nbsp;Understand a call as function(arguments), then inspect the result without creating resources.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Functions transform values inside expressions. The&nbsp;<code>terraform console<\/code>&nbsp;REPL evaluates them interactively; no provider or cloud account is needed. This first config simply mirrors the results as outputs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  name  = lower(\"AZURE\")\n  label = \"${upper(\"dev\")}-${length(\"azure\")}\"\n}\n\noutput \"name\"  { value = local.name }\noutput \"label\" { value = local.label }\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-01-console-first-functions init -backend=false\nterraform -chdir=labs\/lab-01-console-first-functions fmt\nterraform -chdir=labs\/lab-01-console-first-functions validate\nterraform -chdir=labs\/lab-01-console-first-functions plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-01-console-first-functions apply -auto-approve\nterraform -chdir=labs\/lab-01-console-first-functions output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>lower(\"AZURE\")<\/code>&nbsp;becomes&nbsp;<code>azure<\/code>; the label contains&nbsp;<code>DEV-5<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Instructor note:<\/strong>&nbsp;Try&nbsp;<code>lower(\"AZURE\")<\/code>,&nbsp;<code>max(1, 9, 2)<\/code>, and&nbsp;<code>length([\"a\",\"b\"])<\/code>&nbsp;in&nbsp;<code>terraform console<\/code>&nbsp;before planning.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Run&nbsp;<code>terraform console<\/code>&nbsp;and calculate&nbsp;<code>upper(\"linux\")<\/code>&nbsp;and&nbsp;<code>length(\"terraform\")<\/code>&nbsp;yourself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 02 \u2014 Variables, expressions, and function evaluation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-02-variables-vs-functions\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>var.*, trimspace(), lower(), format()<\/code><br><strong>Objective:<\/strong>&nbsp;Distinguish input values from computed expressions; use a function to make a predictable name.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A variable receives a value; an expression evaluates references\/operators\/functions; a function returns a result.&nbsp;<code>locals<\/code>&nbsp;names the derived result. No resource is required to understand any of these.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nvariable \"project\" {\n  type    = string\n  default = \"  WebApp  \"\n}\nvariable \"environment\" {\n  type    = string\n  default = \"dev\"\n}\n\nlocals {\n  clean_project = lower(trimspace(var.project))\n  resource_name = format(\"vm-%s-%s\", local.clean_project, var.environment)\n}\n\noutput \"original_input\" { value = var.project }\noutput \"resource_name\"  { value = local.resource_name }\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-02-variables-vs-functions init -backend=false\nterraform -chdir=labs\/lab-02-variables-vs-functions fmt\nterraform -chdir=labs\/lab-02-variables-vs-functions validate\nterraform -chdir=labs\/lab-02-variables-vs-functions plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-02-variables-vs-functions apply -auto-approve\nterraform -chdir=labs\/lab-02-variables-vs-functions output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>resource_name<\/code>&nbsp;becomes&nbsp;<code>vm-webapp-dev<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change&nbsp;<code>project<\/code>&nbsp;to&nbsp;<code>Billing Portal&nbsp;<\/code>and predict the generated VM name before planning.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Module 2 \u2014 String Functions<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Goal:<\/strong>&nbsp;Clean names and generate predictable configuration text.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use short chains rather than complicated nested expressions. A typical safe pipeline is&nbsp;<code>lower(replace(trimspace(var.project), \" \", \"-\"))<\/code>. A normalized string is not automatically a valid resource name: provider-specific naming, max length, reserved words, and uniqueness requirements still apply.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>trim<\/code>&nbsp;takes a&nbsp;<strong>cutset<\/strong>&nbsp;rather than a literal string.&nbsp;<code>regex<\/code>&nbsp;raises an error when nothing matches, whereas&nbsp;<code>regexall<\/code>&nbsp;returns an empty list. Prefer&nbsp;<code>startswith<\/code>,&nbsp;<code>endswith<\/code>,&nbsp;<code>contains<\/code>,&nbsp;<code>replace<\/code>&nbsp;or other simpler functions instead of regular expressions where possible.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 03 \u2014 Case, trimming, and newline cleanup<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-03-string-case-cleanup\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>lower(), upper(), title(), trim(), trimspace(), chomp()<\/code><br><strong>Objective:<\/strong>&nbsp;Normalize environment labels, an Azure-style resource name, and a copied string.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>trim(value, cutset)<\/code>&nbsp;removes characters belonging to the supplied cutset from both ends, not a literal prefix;&nbsp;<code>trimspace<\/code>&nbsp;removes surrounding Unicode whitespace;&nbsp;<code>chomp<\/code>&nbsp;removes newline endings.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  clean_environment = lower(trimspace(\"  PRODUCTION  \"))\n  pretty_title      = title(\"terraform functions\")\n  shout             = upper(\"azure linux vm\")\n  cut_markers       = trim(\"***dev***\", \"*\")\n  clean_line        = chomp(\"hello\\n\\n\")\n}\n\noutput \"string_results\" {\n  value = {\n    clean_environment = local.clean_environment\n    pretty_title = local.pretty_title\n    shout = local.shout\n    cut_markers = local.cut_markers\n    clean_line = local.clean_line\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-03-string-case-cleanup init -backend=false\nterraform -chdir=labs\/lab-03-string-case-cleanup fmt\nterraform -chdir=labs\/lab-03-string-case-cleanup validate\nterraform -chdir=labs\/lab-03-string-case-cleanup plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-03-string-case-cleanup apply -auto-approve\nterraform -chdir=labs\/lab-03-string-case-cleanup output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>clean_environment<\/code>&nbsp;is&nbsp;<code>production<\/code>,&nbsp;<code>pretty_title<\/code>&nbsp;is&nbsp;<code>Terraform Functions<\/code>,&nbsp;<code>clean_line<\/code>&nbsp;is&nbsp;<code>hello<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Try&nbsp;<code>trim(\"---dev---\", \"-\")<\/code>. Compare&nbsp;<code>chomp(\"x\\n\")<\/code>&nbsp;with&nbsp;<code>trimspace(\" x \\n\")<\/code>&nbsp;in the console.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 04 \u2014 Replace, slice, split, join, and prefix\/suffix checks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-04-string-transform-and-split\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>replace(), substr(), split(), join(), startswith(), endswith()<\/code><br><strong>Objective:<\/strong>&nbsp;Turn an application label into an infrastructure-friendly name, and process a simple comma-separated value.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>substr<\/code>&nbsp;uses a zero-based offset and count.&nbsp;<code>startswith<\/code>\/<code>endswith<\/code>&nbsp;produce booleans;&nbsp;<code>split<\/code>&nbsp;produces a list and&nbsp;<code>join<\/code>&nbsp;rebuilds a string.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  raw_name   = \"My Linux VM\"\n  slug       = lower(replace(local.raw_name, \" \", \"-\"))\n  short_name = substr(\"terraform\", 0, 5)\n  regions    = split(\",\", \"eastus,westeurope,japaneast\")\n  label      = join(\"-\", &#91;\"vm\", \"demo\", \"dev\"])\n  is_vm      = startswith(local.label, \"vm-\")\n  is_dev     = endswith(local.label, \"-dev\")\n}\n\noutput \"transforms\" {\n  value = {\n    raw_name = local.raw_name\n    slug = local.slug\n    short_name = local.short_name\n    regions = local.regions\n    label = local.label\n    is_vm = local.is_vm\n    is_dev = local.is_dev\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-04-string-transform-and-split init -backend=false\nterraform -chdir=labs\/lab-04-string-transform-and-split fmt\nterraform -chdir=labs\/lab-04-string-transform-and-split validate\nterraform -chdir=labs\/lab-04-string-transform-and-split plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-04-string-transform-and-split apply -auto-approve\nterraform -chdir=labs\/lab-04-string-transform-and-split output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>slug<\/code>&nbsp;is&nbsp;<code>my-linux-vm<\/code>,&nbsp;<code>short_name<\/code>&nbsp;is&nbsp;<code>terra<\/code>,&nbsp;<code>regions<\/code>&nbsp;has three elements, and both checks are true.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change&nbsp;<code>raw_name<\/code>&nbsp;to&nbsp;<code>Data Service<\/code>; add&nbsp;<code>\"japaneast\"<\/code>&nbsp;to the region list.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 05 \u2014 Formatting values and finding text patterns<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-05-formatting-and-regex\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>format(), formatlist(), regex(), regexall()<\/code><br><strong>Objective:<\/strong>&nbsp;Generate consistent VM labels and extract parts of a resource identifier.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>format<\/code>&nbsp;returns one string;&nbsp;<code>formatlist<\/code>&nbsp;applies one format across each list element.&nbsp;<code>regex<\/code>&nbsp;errors when no match exists;&nbsp;<code>regexall<\/code>&nbsp;returns an empty list if there are no matches. Use a simpler dedicated function when possible.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  single       = format(\"vm-%s-%02d\", \"dev\", 3)\n  several      = formatlist(\"node-%02d\", &#91;1, 2, 3])\n  first_word   = regex(\"&#91;a-z]+\", \"rg-prod-03\")\n  digit_groups = regexall(\"&#91;0-9]+\", \"vm12-zone3\")\n  is_valid     = length(regexall(\"^&#91;a-z]&#91;a-z0-9-]*$\", \"my-linux-vm\")) &gt; 0\n}\n\noutput \"formatted\" {\n  value = {\n    single = local.single\n    several = local.several\n    first_word = local.first_word\n    digit_groups = local.digit_groups\n    is_valid = local.is_valid\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-05-formatting-and-regex init -backend=false\nterraform -chdir=labs\/lab-05-formatting-and-regex fmt\nterraform -chdir=labs\/lab-05-formatting-and-regex validate\nterraform -chdir=labs\/lab-05-formatting-and-regex plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-05-formatting-and-regex apply -auto-approve\nterraform -chdir=labs\/lab-05-formatting-and-regex output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>single<\/code>&nbsp;is&nbsp;<code>vm-dev-03<\/code>;&nbsp;<code>several<\/code>&nbsp;contains three node labels;&nbsp;<code>digit_groups<\/code>&nbsp;is&nbsp;<code>[\"12\", \"3\"]<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Try&nbsp;<code>regexall(\"[0-9]+\", \"no-digits\")<\/code>. Why is this safer than&nbsp;<code>regex<\/code>&nbsp;for counting matches?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Module 3 \u2014 Collection Functions<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Goal:<\/strong>&nbsp;Combine, normalize and select list\/set\/map values without resources.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Lists<\/strong>&nbsp;are ordered;&nbsp;<strong>sets<\/strong>&nbsp;contain unique values and have no promised ordering;&nbsp;<strong>maps<\/strong>&nbsp;associate string keys with values.&nbsp;<code>tolist(toset(x))<\/code>&nbsp;removes duplicate items but does not guarantee a consistent order; use&nbsp;<code>sort(tolist(toset(x)))<\/code>&nbsp;when stable naming or plan rendering matters.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>element<\/code>&nbsp;has intentional wrap-around behavior; direct&nbsp;<code>[index]<\/code>&nbsp;access is usually clearer.&nbsp;<code>lookup(map,key,default)<\/code>&nbsp;avoids missing-key errors, and later arguments to&nbsp;<code>merge<\/code>&nbsp;override the earlier map values.&nbsp;<code>matchkeys<\/code>&nbsp;is useful for parallel lists but is often less readable than a&nbsp;<code>for<\/code>&nbsp;expression.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 06 \u2014 Count, access, join, and flatten collections<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-06-list-basics\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>length(), element(), concat(), flatten()<\/code><br><strong>Objective:<\/strong>&nbsp;Work with a simple list of Azure deployment environments and nested collections.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Lists are zero-indexed.&nbsp;<code>element<\/code>&nbsp;wraps an out-of-range index (unlike&nbsp;<code>list[index]<\/code>), so prefer ordinary indexing unless wraparound is intended.&nbsp;<code>flatten<\/code>&nbsp;removes nesting of directly nested lists.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  environments = &#91;\"dev\", \"stage\", \"prod\"]\n  count        = length(local.environments)\n  wraparound   = element(&#91;\"eastus\", \"westus\"], 3)\n  together     = concat(&#91;\"dev\"], &#91;\"stage\", \"prod\"])\n  flat         = flatten(&#91;&#91;\"dev\", \"qa\"], &#91;\"stage\", \"prod\"]])\n}\n\noutput \"collections\" {\n  value = {\n    environments = local.environments\n    count = local.count\n    wraparound = local.wraparound\n    together = local.together\n    flat = local.flat\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-06-list-basics init -backend=false\nterraform -chdir=labs\/lab-06-list-basics fmt\nterraform -chdir=labs\/lab-06-list-basics validate\nterraform -chdir=labs\/lab-06-list-basics plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-06-list-basics apply -auto-approve\nterraform -chdir=labs\/lab-06-list-basics output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>count<\/code>&nbsp;is&nbsp;<code>3<\/code>,&nbsp;<code>wraparound<\/code>&nbsp;is&nbsp;<code>westus<\/code>, and&nbsp;<code>flat<\/code>&nbsp;has four strings.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Why does&nbsp;<code>element([\"eastus\", \"westus\"], 3)<\/code>&nbsp;return the second element?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 07 \u2014 Deduplicate, filter blanks, search, and convert types<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-07-clean-collections\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>distinct(), compact(), contains(), index(), tolist(), toset(), tomap()<\/code><br><strong>Objective:<\/strong>&nbsp;Clean an input list of environments without&nbsp;<code>for_each<\/code>&nbsp;or resources.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Sets remove duplicates and do not promise iteration order. Convert with&nbsp;<code>tolist<\/code>&nbsp;and&nbsp;<code>sort<\/code>&nbsp;when a stable order matters.&nbsp;<code>index<\/code>&nbsp;errors if an item is missing;&nbsp;<code>contains<\/code>&nbsp;is safer for testing membership.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  raw            = &#91;\"dev\", \"\", \"dev\", \"prod\", null]\n  nonempty       = compact(local.raw)\n  deduplicated   = distinct(local.nonempty)\n  unique_sorted  = sort(tolist(toset(local.nonempty)))\n  labels_by_env  = tomap({ dev = \"Linux\", prod = \"Linux\" })\n  found_prod     = contains(local.deduplicated, \"prod\")\n  prod_index     = index(local.deduplicated, \"prod\")\n}\n\noutput \"cleaned\" {\n  value = {\n    raw = local.raw\n    nonempty = local.nonempty\n    deduplicated = local.deduplicated\n    unique_sorted = local.unique_sorted\n    labels_by_env = local.labels_by_env\n    found_prod = local.found_prod\n    prod_index = local.prod_index\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-07-clean-collections init -backend=false\nterraform -chdir=labs\/lab-07-clean-collections fmt\nterraform -chdir=labs\/lab-07-clean-collections validate\nterraform -chdir=labs\/lab-07-clean-collections plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-07-clean-collections apply -auto-approve\nterraform -chdir=labs\/lab-07-clean-collections output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>deduplicated<\/code>&nbsp;is&nbsp;<code>[\"dev\", \"prod\"]<\/code>;&nbsp;<code>found_prod<\/code>&nbsp;is true;&nbsp;<code>prod_index<\/code>&nbsp;is 1.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Try&nbsp;<code>index([\"dev\",\"prod\"], \"stage\")<\/code>&nbsp;in the console. How could you avoid the error?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 08 \u2014 Look up, merge, zip, match and order data<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-08-maps-and-ordering\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>keys(), values(), lookup(), merge(), zipmap(), matchkeys(), sort(), reverse()<\/code><br><strong>Objective:<\/strong>&nbsp;Build a reusable environment-to-VM-size map and select values by key.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>merge<\/code>&nbsp;uses values from the later map when a key repeats.&nbsp;<code>keys<\/code>&nbsp;and&nbsp;<code>values<\/code>&nbsp;return results sorted by key, while&nbsp;<code>matchkeys(values, keys, searchset)<\/code>&nbsp;keeps matching positions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  sizes        = zipmap(&#91;\"dev\", \"prod\"], &#91;\"Standard_B1s\", \"Standard_B2s_v2\"])\n  size         = lookup(local.sizes, \"stage\", \"Standard_B1s\")\n  all_keys     = keys(local.sizes)\n  all_values   = values(local.sizes)\n  common_tags  = merge({ team = \"platform\", env = \"dev\" }, { env = \"prod\" })\n  matched      = matchkeys(&#91;\"vm-a\", \"vm-b\", \"vm-c\"], &#91;\"dev\", \"prod\", \"dev\"], &#91;\"dev\"])\n  ascending    = sort(&#91;\"prod\", \"dev\", \"stage\"])\n  descending   = reverse(local.ascending)\n}\n\noutput \"map_results\" {\n  value = {\n    sizes = local.sizes\n    size = local.size\n    all_keys = local.all_keys\n    all_values = local.all_values\n    common_tags = local.common_tags\n    matched = local.matched\n    ascending = local.ascending\n    descending = local.descending\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-08-maps-and-ordering init -backend=false\nterraform -chdir=labs\/lab-08-maps-and-ordering fmt\nterraform -chdir=labs\/lab-08-maps-and-ordering validate\nterraform -chdir=labs\/lab-08-maps-and-ordering plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-08-maps-and-ordering apply -auto-approve\nterraform -chdir=labs\/lab-08-maps-and-ordering output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>size<\/code>&nbsp;is&nbsp;<code>Standard_B1s<\/code>;&nbsp;<code>matched<\/code>&nbsp;contains&nbsp;<code>[\"vm-a\", \"vm-c\"]<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change the requested environment to&nbsp;<code>prod<\/code>. Predict&nbsp;<code>lookup<\/code>&nbsp;with an unknown key and a default value.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Module 4 \u2014 Numeric and Mathematical Functions<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Goal:<\/strong>&nbsp;Calculate capacities, numeric bounds and sequences.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use math for infrastructure policies where the algorithm is obvious, e.g.&nbsp;<code>ceil(var.expected_users \/ 50)<\/code>&nbsp;for a&nbsp;<em>demonstration<\/em>&nbsp;VM count. Real sizing requires memory, CPU and load measurement, so the quotient is an educational model rather than an AWS\/Azure sizing recommendation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>max<\/code>&nbsp;and&nbsp;<code>min<\/code>&nbsp;accept numbers as individual arguments; when starting from a list use expansion syntax like&nbsp;<code>max([1, 4, 3]...)<\/code>.&nbsp;<code>range<\/code>&nbsp;can be used to generate list indices, but Terraform limits the maximum size of generated ranges; don&#8217;t use it to create an uncontrolled number of resources.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 09 \u2014 Normalize and compare numeric values<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-09-numeric-foundations\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>abs(), ceil(), floor(), max(), min(), parseint(), signum()<\/code><br><strong>Objective:<\/strong>&nbsp;Round estimated capacity, compare resource counts, and convert number strings.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>ceil<\/code>&nbsp;rounds up,&nbsp;<code>floor<\/code>&nbsp;rounds down;&nbsp;<code>parseint<\/code>&nbsp;takes a digit string and its radix;&nbsp;<code>signum<\/code>&nbsp;returns -1, 0, or 1.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nvariable \"expected_users\" {\n  type    = number\n  default = 120\n}\n\nlocals {\n  required_vms  = ceil(var.expected_users \/ 50)\n  absolute      = abs(-7)\n  rounded_down  = floor(2.9)\n  biggest       = max(3, 8, 5)\n  smallest      = min(3, 8, 5)\n  hex_to_number = parseint(\"ff\", 16)\n  direction     = signum(-42)\n}\n\noutput \"math_results\" {\n  value = {\n    required_vms = local.required_vms\n    absolute = local.absolute\n    rounded_down = local.rounded_down\n    biggest = local.biggest\n    smallest = local.smallest\n    hex_to_number = local.hex_to_number\n    direction = local.direction\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-09-numeric-foundations init -backend=false\nterraform -chdir=labs\/lab-09-numeric-foundations fmt\nterraform -chdir=labs\/lab-09-numeric-foundations validate\nterraform -chdir=labs\/lab-09-numeric-foundations plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-09-numeric-foundations apply -auto-approve\nterraform -chdir=labs\/lab-09-numeric-foundations output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>required_vms<\/code>&nbsp;is 3 for 120 users at 50 users per VM;&nbsp;<code>hex_to_number<\/code>&nbsp;is 255.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change&nbsp;<code>expected_users<\/code>&nbsp;to 151; predict the new capacity. What should happen if users is negative?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 10 \u2014 Powers, logarithms, totals, and number ranges<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-10-math-sequences\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>log(), pow(), sum(), range()<\/code><br><strong>Objective:<\/strong>&nbsp;Calculate a sample capacity series and a simple scale-up estimate.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>log(x, base)<\/code>&nbsp;calculates a logarithm;&nbsp;<code>pow(x, y)<\/code>&nbsp;raises to a power;&nbsp;<code>sum<\/code>&nbsp;totals a collection of numbers;&nbsp;<code>range(start, limit, step)<\/code>&nbsp;stops before the limit and has a maximum output size.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  steps          = range(0, 4)\n  powers_of_two  = &#91;for i in local.steps : pow(2, i)]\n  total_capacity = sum(local.powers_of_two)\n  log_example    = log(100, 10)\n  odd_indices    = range(1, 7, 2)\n}\n\noutput \"math_results\" {\n  value = {\n    steps = local.steps\n    powers_of_two = local.powers_of_two\n    total_capacity = local.total_capacity\n    log_example = local.log_example\n    odd_indices = local.odd_indices\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-10-math-sequences init -backend=false\nterraform -chdir=labs\/lab-10-math-sequences fmt\nterraform -chdir=labs\/lab-10-math-sequences validate\nterraform -chdir=labs\/lab-10-math-sequences plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-10-math-sequences apply -auto-approve\nterraform -chdir=labs\/lab-10-math-sequences output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>powers_of_two<\/code>&nbsp;is&nbsp;<code>[1, 2, 4, 8]<\/code>,&nbsp;<code>total_capacity<\/code>&nbsp;is 15,&nbsp;<code>odd_indices<\/code>&nbsp;is&nbsp;<code>[1,3,5]<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use&nbsp;<code>range(1, 8, 2)<\/code>&nbsp;to create odd numbers. Why should you avoid unbounded huge ranges?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Module 5 \u2014 Type Conversion, Validation, and Error Handling<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Goal:<\/strong>&nbsp;Safely handle optional values and invalid input.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Terraform already performs many type conversions automatically. Explicit&nbsp;<code>tostring<\/code>,&nbsp;<code>tonumber<\/code>, and&nbsp;<code>tobool<\/code>&nbsp;help when normalizing values read from JSON\/YAML or returned by providers.&nbsp;<code>tobool<\/code>&nbsp;accepts only booleans, null, and the strings&nbsp;<code>\"true\"<\/code>&nbsp;and&nbsp;<code>\"false\"<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>try()<\/code>&nbsp;and&nbsp;<code>can()<\/code>&nbsp;are for&nbsp;<strong>dynamic<\/strong>&nbsp;errors, not syntax mistakes; use&nbsp;<code>try<\/code>&nbsp;for a fallback result and&nbsp;<code>can<\/code>&nbsp;for a boolean check.&nbsp;<code>coalesce<\/code>&nbsp;skips null and empty-string arguments;&nbsp;<code>coalescelist<\/code>&nbsp;skips empty lists.&nbsp;<code>one<\/code>&nbsp;is appropriate when a collection should have at most one item.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>sensitive()<\/code>&nbsp;marks a value for redaction in normal Terraform UI output. It does&nbsp;<strong>not<\/strong>&nbsp;encrypt or remove it from state and saved plans.&nbsp;<code>nonsensitive()<\/code>&nbsp;deliberately removes that mark, and therefore must never be used casually on an actual credential.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>flowchart TD\n  A&#91;Optional input] --&gt; B{Value present and valid?}\n  B --&gt;|Yes| C&#91;Use input]\n  B --&gt;|No| D&#91;Use try \/ lookup \/ coalesce]\n  D --&gt; E&#91;Validate final input]\n  C --&gt; E\n  E --&gt; F&#91;Derived configuration]\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 11 \u2014 Conversions, safe lookups, and nulls<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-11-conversion-and-fallbacks\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>tostring(), tonumber(), tobool(), try(), can(), coalesce(), coalescelist(), one()<\/code><br><strong>Objective:<\/strong>&nbsp;Handle optional settings and data read from text without creating any resources.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>try<\/code>&nbsp;catches dynamically arising errors, not a completely invalid static expression.&nbsp;<code>can<\/code>&nbsp;is useful in variable validation.&nbsp;<code>coalesce<\/code>&nbsp;skips null and empty strings;&nbsp;<code>coalescelist<\/code>&nbsp;picks the first nonempty list;&nbsp;<code>one<\/code>&nbsp;allows zero or one item only.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  settings   = { environment = \"dev\" }\n  count_text = tostring(3)\n  count_num  = tonumber(\"3\")\n  enabled    = tobool(\"true\")\n  size       = try(local.settings.instance.size, \"Standard_B1s\")\n  has_number = can(tonumber(\"not-a-number\"))\n  primary    = coalesce(null, \"\", \"ops@example.com\")\n  zones      = coalescelist(&#91;], &#91;\"zone-a\", \"zone-b\"])\n  only_item  = one(&#91;\"web\"])\n  no_item    = one(&#91;])\n}\n\noutput \"safe_values\" {\n  value = {\n    settings = local.settings\n    count_text = local.count_text\n    count_num = local.count_num\n    enabled = local.enabled\n    size = local.size\n    has_number = local.has_number\n    primary = local.primary\n    zones = local.zones\n    only_item = local.only_item\n    no_item = local.no_item\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-11-conversion-and-fallbacks init -backend=false\nterraform -chdir=labs\/lab-11-conversion-and-fallbacks fmt\nterraform -chdir=labs\/lab-11-conversion-and-fallbacks validate\nterraform -chdir=labs\/lab-11-conversion-and-fallbacks plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-11-conversion-and-fallbacks apply -auto-approve\nterraform -chdir=labs\/lab-11-conversion-and-fallbacks output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>size<\/code>&nbsp;is&nbsp;<code>Standard_B1s<\/code>,&nbsp;<code>has_number<\/code>&nbsp;is false,&nbsp;<code>primary<\/code>&nbsp;is&nbsp;<code>ops@example.com<\/code>,&nbsp;<code>only_item<\/code>&nbsp;is&nbsp;<code>web<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Test&nbsp;<code>tobool(\"yes\")<\/code>&nbsp;and&nbsp;<code>one([\"dev\",\"prod\"])<\/code>&nbsp;in the console. Explain why they fail.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 12 \u2014 Variable validation and safe sensitive marking<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-12-validation-and-sensitive\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>can(), regex(), contains(), sensitive(), nonsensitive()<\/code><br><strong>Objective:<\/strong>&nbsp;Validate environment choices and demonstrate that sensitive marking affects CLI display, not state secrecy.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>validation<\/code>&nbsp;blocks reject invalid inputs. A value passed into&nbsp;<code>sensitive()<\/code>&nbsp;is redacted in normal CLI output, but may still be present in state.&nbsp;<code>nonsensitive()<\/code>&nbsp;must only be used after intentionally proving the value safe for exposure.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nvariable \"environment\" {\n  type    = string\n  default = \"dev\"\n  validation {\n    condition     = contains(&#91;\"dev\", \"stage\", \"prod\"], var.environment)\n    error_message = \"environment must be dev, stage, or prod.\"\n  }\n}\n\nvariable \"application\" {\n  type    = string\n  default = \"linuxapp\"\n  validation {\n    condition     = can(regex(\"^&#91;a-z]&#91;a-z0-9-]+$\", var.application))\n    error_message = \"application must contain lowercase letters, digits or hyphens.\"\n  }\n}\n\nlocals {\n  demo_sensitive = sensitive(\"TRAINING-ONLY-NOT-A-REAL-SECRET\")\n  safe_public    = nonsensitive(sensitive(\"public-label\"))\n}\n\noutput \"validated_name\" { value = \"${var.application}-${var.environment}\" }\noutput \"public_label\"   { value = local.safe_public }\noutput \"training_secret\" {\n  value     = local.demo_sensitive\n  sensitive = true\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-12-validation-and-sensitive init -backend=false\nterraform -chdir=labs\/lab-12-validation-and-sensitive fmt\nterraform -chdir=labs\/lab-12-validation-and-sensitive validate\nterraform -chdir=labs\/lab-12-validation-and-sensitive plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-12-validation-and-sensitive apply -auto-approve\nterraform -chdir=labs\/lab-12-validation-and-sensitive output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>environment<\/code>&nbsp;is&nbsp;<code>dev<\/code>; the secret-like training output is shown as sensitive. Changing environment to&nbsp;<code>invalid<\/code>&nbsp;fails validation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Run&nbsp;<code>terraform plan -var=\"environment=invalid\"<\/code>. Do not use real secrets in examples or commit state.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Module 6 \u2014 File, Encoding, and Data Functions<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Goal:<\/strong>&nbsp;Work with static files, JSON\/YAML and cloud-init payloads.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>file<\/code>&nbsp;and&nbsp;<code>templatefile<\/code>&nbsp;read&nbsp;<strong>pre-existing<\/strong>&nbsp;files; Terraform functions do not participate in the resource dependency graph, so they cannot reliably read files a resource creates during the same operation. Always prefer paths built with&nbsp;<code>${path.module}<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>file<\/code>&nbsp;decodes UTF-8 text, while&nbsp;<code>filebase64<\/code>&nbsp;reads bytes and Base64-encodes them.&nbsp;<code>base64decode<\/code>&nbsp;expects Base64-encoded UTF-8 text;&nbsp;<code>textdecodebase64(text, \"UTF-8\")<\/code>&nbsp;specifies the decoding character set. Use&nbsp;<code>jsonencode<\/code>\/<code>yamlencode<\/code>&nbsp;rather than manual string concatenation for structured configuration. For AzureRM&nbsp;<code>azurerm_linux_virtual_machine.custom_data<\/code>, wrap cloud-init text in&nbsp;<code>base64encode(...)<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 13 \u2014 Read local files and encode text safely<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-13-read-files-and-encode\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>file(), fileexists(), filebase64(), base64encode(), base64decode(), textdecodebase64()<\/code><br><strong>Objective:<\/strong>&nbsp;Read one pre-existing text file, test existence, and compare text and byte-oriented Base64 functions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>file<\/code>&nbsp;returns UTF-8 text.&nbsp;<code>filebase64<\/code>&nbsp;encodes the original bytes, including newlines.&nbsp;<code>textdecodebase64<\/code>&nbsp;requires an explicit character encoding. Files must exist before Terraform starts.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  text        = chomp(file(\"${path.module}\/files\/greeting.txt\"))\n  exists      = fileexists(\"${path.module}\/files\/greeting.txt\")\n  missing     = fileexists(\"${path.module}\/files\/not-here.txt\")\n  base64_text = base64encode(\"hello\")\n  decoded     = base64decode(local.base64_text)\n  file_bytes  = filebase64(\"${path.module}\/files\/greeting.txt\")\n  roundtrip   = textdecodebase64(local.file_bytes, \"UTF-8\")\n}\n\noutput \"file_results\" {\n  value = {\n    text = local.text\n    exists = local.exists\n    missing = local.missing\n    base64_text = local.base64_text\n    decoded = local.decoded\n    file_bytes = local.file_bytes\n    roundtrip = local.roundtrip\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>files\/greeting.txt<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>hello\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-13-read-files-and-encode init -backend=false\nterraform -chdir=labs\/lab-13-read-files-and-encode fmt\nterraform -chdir=labs\/lab-13-read-files-and-encode validate\nterraform -chdir=labs\/lab-13-read-files-and-encode plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-13-read-files-and-encode apply -auto-approve\nterraform -chdir=labs\/lab-13-read-files-and-encode output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>exists<\/code>&nbsp;is true,&nbsp;<code>missing<\/code>&nbsp;is false,&nbsp;<code>base64_text<\/code>&nbsp;is&nbsp;<code>aGVsbG8=<\/code>&nbsp;and&nbsp;<code>decoded<\/code>&nbsp;is&nbsp;<code>hello<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change&nbsp;<code>files\/greeting.txt<\/code>&nbsp;to include a second line; observe Base64 output versus&nbsp;<code>chomp(file(...))<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 14 \u2014 Decode and encode JSON and YAML configuration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-14-json-and-yaml\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>jsonencode(), jsondecode(), yamlencode(), yamldecode(), file()<\/code><br><strong>Objective:<\/strong>&nbsp;Read two tiny application configs and produce serialized content for cloud automation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>jsondecode<\/code>&nbsp;and&nbsp;<code>yamldecode<\/code>&nbsp;return Terraform values (objects\/lists\/scalars); the encoding functions produce strings.&nbsp;<code>jsonencode<\/code>&nbsp;is suitable for structured JSON, avoiding hand-built JSON text.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  app          = jsondecode(file(\"${path.module}\/files\/config.json\"))\n  settings     = yamldecode(file(\"${path.module}\/files\/config.yaml\"))\n  application  = local.app.name\n  environment  = local.settings.environment\n  json_output  = jsonencode({ name = local.application, env = local.environment })\n  yaml_output  = yamlencode({ name = local.application, port = local.settings.port })\n}\n\noutput \"application\"  { value = local.application }\noutput \"environment\"  { value = local.environment }\noutput \"json_string\"  { value = local.json_output }\noutput \"yaml_string\"  { value = local.yaml_output }\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>files\/config.json<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\"name\":\"web\",\"team\":\"platform\",\"replicas\":2}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>files\/config.yaml<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>environment: dev\nport: 8080\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-14-json-and-yaml init -backend=false\nterraform -chdir=labs\/lab-14-json-and-yaml fmt\nterraform -chdir=labs\/lab-14-json-and-yaml validate\nterraform -chdir=labs\/lab-14-json-and-yaml plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-14-json-and-yaml apply -auto-approve\nterraform -chdir=labs\/lab-14-json-and-yaml output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>application<\/code>&nbsp;is&nbsp;<code>web<\/code>,&nbsp;<code>environment<\/code>&nbsp;is&nbsp;<code>dev<\/code>, and YAML port is&nbsp;<code>8080<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change&nbsp;<code>config.json<\/code>&nbsp;to add a tag, and add an output for it. Compare structured output with serialized JSON output.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 15 \u2014 Render a cloud-init template and encode its payload<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-15-templatefile-cloud-init\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>templatefile(), base64encode(), fileexists()<\/code><br><strong>Objective:<\/strong>&nbsp;Connect functions to a real-world bootstrap script while remaining cloud-free.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>templatefile(path, vars)<\/code>&nbsp;renders a UTF-8 template from a file that exists before the run. Use explicit variable names in the&nbsp;<code>vars<\/code>&nbsp;object. Azure Linux VM&nbsp;<code>custom_data<\/code>&nbsp;expects Base64 in its newer VM resource. This lab only previews the payload.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nvariable \"environment\" {\n  type    = string\n  default = \"dev\"\n}\n\nlocals {\n  cloud_init = templatefile(\"${path.module}\/cloud-init.tftpl\", {\n    environment = var.environment\n    app_name    = \"web\"\n  })\n}\n\noutput \"cloud_init\"       { value = local.cloud_init }\noutput \"encoded_cloud_init\" { value = base64encode(local.cloud_init) }\noutput \"template_exists\"  { value = fileexists(\"${path.module}\/cloud-init.tftpl\") }\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>cloud-init.tftpl<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em>#cloud-config<\/em>\nwrite_files:\n  - path: \/etc\/functions-training.conf\n    permissions: '0644'\n    content: |\n      environment=${environment}\n      application=${app_name}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-15-templatefile-cloud-init init -backend=false\nterraform -chdir=labs\/lab-15-templatefile-cloud-init fmt\nterraform -chdir=labs\/lab-15-templatefile-cloud-init validate\nterraform -chdir=labs\/lab-15-templatefile-cloud-init plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-15-templatefile-cloud-init apply -auto-approve\nterraform -chdir=labs\/lab-15-templatefile-cloud-init output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The rendered YAML starts with&nbsp;<code>#cloud-config<\/code>&nbsp;and writes a file containing&nbsp;<code>environment=dev<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change the environment to&nbsp;<code>stage<\/code>; inspect&nbsp;<code>terraform output -raw cloud_init<\/code>. Explain why&nbsp;<code>base64encode<\/code>&nbsp;is needed for the Azure Linux VM resource.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Module 7 \u2014 Filesystem, Path, and Time Functions<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Goal:<\/strong>&nbsp;Use portable paths and understand time-dependent values.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>path.module<\/code>&nbsp;is the directory containing the current module;&nbsp;<code>path.root<\/code>&nbsp;is the root module;&nbsp;<code>path.cwd<\/code>&nbsp;is Terraform&#8217;s original working directory, which may differ when&nbsp;<code>-chdir<\/code>&nbsp;is used.&nbsp;<code>abspath<\/code>&nbsp;and&nbsp;<code>pathexpand<\/code>&nbsp;depend on the machine running Terraform, so do not embed their results in shared resource names or remote identifiers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>plantimestamp()<\/code>&nbsp;returns a fixed timestamp within a plan; it changes on later plans and is&nbsp;<strong>not available in the console<\/strong>.&nbsp;<code>timestamp()<\/code>&nbsp;returns an apply-time current value and is unknown in plan. The two are not interchangeable, and neither should be attached to an otherwise stable resource name.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 16 \u2014 Filesystem path functions and globbed file sets<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-16-paths-and-fileset\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>abspath(), dirname(), basename(), pathexpand(), fileset(), path.module, path.root, path.cwd<\/code><br><strong>Objective:<\/strong>&nbsp;Use filesystem functions predictably, resolving files relative to the module rather than to the shell working directory.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>path.module<\/code>&nbsp;locates the current module,&nbsp;<code>path.root<\/code>&nbsp;the root module and&nbsp;<code>path.cwd<\/code>&nbsp;the original working directory. Absolute\/home-expanded paths vary across learners, so never use them as stable infrastructure identifiers.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  example_path  = \"${path.module}\/files\/dev.yaml\"\n  absolute_path = abspath(local.example_path)\n  directory     = dirname(local.example_path)\n  file_name     = basename(local.example_path)\n  home_example  = pathexpand(\"~\/.ssh\/id_ed25519.pub\")\n  yaml_files    = sort(tolist(fileset(\"${path.module}\/files\", \"*.yaml\")))\n  module_path   = path.module\n  root_path     = path.root\n  cwd_path      = path.cwd\n}\n\noutput \"path_results\" {\n  value = {\n    example_path = local.example_path\n    absolute_path = local.absolute_path\n    directory = local.directory\n    file_name = local.file_name\n    home_example = local.home_example\n    yaml_files = local.yaml_files\n    module_path = local.module_path\n    root_path = local.root_path\n    cwd_path = local.cwd_path\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>files\/dev.yaml<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>environment: dev\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>files\/prod.yaml<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>environment: prod\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-16-paths-and-fileset init -backend=false\nterraform -chdir=labs\/lab-16-paths-and-fileset fmt\nterraform -chdir=labs\/lab-16-paths-and-fileset validate\nterraform -chdir=labs\/lab-16-paths-and-fileset plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-16-paths-and-fileset apply -auto-approve\nterraform -chdir=labs\/lab-16-paths-and-fileset output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>yaml_files<\/code>&nbsp;contains&nbsp;<code>[\"dev.yaml\", \"prod.yaml\"]<\/code>&nbsp;(sorted); basename is&nbsp;<code>dev.yaml<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">From the parent folder run&nbsp;<code>terraform -chdir=&lt;lab&gt; plan<\/code>. Why is&nbsp;<code>${path.module}<\/code>&nbsp;safer than a relative string alone?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 17 \u2014 Time calculations and plan-time versus apply-time values<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-17-time-and-plan-timestamp\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>timestamp(), timeadd(), plantimestamp(), formatdate()<\/code><br><strong>Objective:<\/strong>&nbsp;Distinguish stable date transformations from values that change between Terraform runs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>timestamp()<\/code>&nbsp;is unknown during plan and changes each apply;&nbsp;<code>plantimestamp()<\/code>&nbsp;is fixed for a plan but differs between plans and cannot be evaluated in&nbsp;<code>terraform console<\/code>. Do not use either to form stable resource names or tags. The static time calculation is deterministic.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nlocals {\n  fixed_time    = \"2026-10-14T10:00:00Z\"\n  tomorrow      = timeadd(local.fixed_time, \"24h\")\n  readable_date = formatdate(\"YYYY-MM-DD\", local.fixed_time)\n}\n\noutput \"tomorrow\"      { value = local.tomorrow }\noutput \"readable_date\" { value = local.readable_date }\noutput \"plan_time\"     { value = plantimestamp() }\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-17-time-and-plan-timestamp init -backend=false\nterraform -chdir=labs\/lab-17-time-and-plan-timestamp fmt\nterraform -chdir=labs\/lab-17-time-and-plan-timestamp validate\nterraform -chdir=labs\/lab-17-time-and-plan-timestamp plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-17-time-and-plan-timestamp apply -auto-approve\nterraform -chdir=labs\/lab-17-time-and-plan-timestamp output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>tomorrow<\/code>&nbsp;is&nbsp;<code>2026-10-15T10:00:00Z<\/code>&nbsp;and&nbsp;<code>readable_date<\/code>&nbsp;is&nbsp;<code>2026-10-14<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Instructor note:<\/strong>&nbsp;<code>plantimestamp()<\/code>&nbsp;is not supported inside console; use&nbsp;<code>terraform plan<\/code>.&nbsp;<code>timestamp()<\/code>&nbsp;is demonstrated in the console only.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Inspect&nbsp;<code>terraform plan<\/code>&nbsp;twice and compare plan time; then run&nbsp;<code>timestamp()<\/code>&nbsp;in console. Do&nbsp;<strong>not<\/strong>&nbsp;try&nbsp;<code>plantimestamp()<\/code>&nbsp;in console.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Module 8 \u2014 Advanced Functions and Real-World Use Cases<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Goal:<\/strong>&nbsp;Compose functions for cloud addressing, digests, stable names, and optional Azure\/AWS resources.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>cidrsubnet<\/code>&nbsp;and&nbsp;<code>cidrsubnets<\/code>&nbsp;produce network ranges; once those ranges are assigned to real networks, changing the inputs may force subnet replacement and disrupt connectivity. Use&nbsp;<code>sha256<\/code>\/<code>md5<\/code>&nbsp;for checksums and change detection, not for password security.&nbsp;<code>uuid()<\/code>&nbsp;creates a random value on apply, while&nbsp;<code>uuidv5<\/code>&nbsp;produces a stable name-based UUID.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Provider-defined functions use&nbsp;<code>provider::&lt;local-name&gt;::&lt;function&gt;<\/code>. Unlike most built-in functions, they require a provider declaration. The built-in&nbsp;<code>terraform<\/code>&nbsp;provider&#8217;s&nbsp;<code>encode_tfvars<\/code>&nbsp;function is a good beginner demonstration requiring no cloud provider download.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The last three labs separate&nbsp;<strong>local Azure preview<\/strong>,&nbsp;<strong>optional live Azure Ubuntu VM<\/strong>, and&nbsp;<strong>optional AWS VPC\/subnet<\/strong>. They are deliberately separate from your earlier 13-resource Azure variables lab to protect existing Terraform state.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 18 \u2014 CIDR math, digests, and stable unique identifiers<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-18-cidr-hashes-and-uuids\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>cidrsubnet(), cidrsubnets(), md5(), sha256(), filemd5(), uuid(), uuidv5()<\/code><br><strong>Objective:<\/strong>&nbsp;Derive Azure\/AWS subnet addresses and compute stable fingerprints.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>cidrsubnet<\/code>&nbsp;selects one numbered child network;&nbsp;<code>cidrsubnets<\/code>&nbsp;allocates consecutive child networks.&nbsp;<code>md5<\/code>&nbsp;and&nbsp;<code>sha256<\/code>&nbsp;produce digests, not encryption; never hash passwords as a substitute for password hashing.&nbsp;<code>uuid()<\/code>&nbsp;is non-deterministic at apply;&nbsp;<code>uuidv5<\/code>&nbsp;derives a stable value from a namespace and name.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nvariable \"network_cidr\" {\n  type    = string\n  default = \"10.20.0.0\/16\"\n}\n\nlocals {\n  subnet_one  = cidrsubnet(var.network_cidr, 8, 1)\n  subnet_pair = cidrsubnets(var.network_cidr, 8, 8)\n  text_md5    = md5(\"hello\")\n  text_sha256 = sha256(\"hello\")\n  file_md5    = filemd5(\"${path.module}\/files\/release.txt\")\n  stable_uuid = uuidv5(\"dns\", \"app.example.org\")\n}\n\noutput \"network_and_identity\" {\n  value = {\n    subnet_one = local.subnet_one\n    subnet_pair = local.subnet_pair\n    text_md5 = local.text_md5\n    text_sha256 = local.text_sha256\n    file_md5 = local.file_md5\n    stable_uuid = local.stable_uuid\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>files\/release.txt<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>release=v1\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-18-cidr-hashes-and-uuids init -backend=false\nterraform -chdir=labs\/lab-18-cidr-hashes-and-uuids fmt\nterraform -chdir=labs\/lab-18-cidr-hashes-and-uuids validate\nterraform -chdir=labs\/lab-18-cidr-hashes-and-uuids plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-18-cidr-hashes-and-uuids apply -auto-approve\nterraform -chdir=labs\/lab-18-cidr-hashes-and-uuids output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>subnet_one<\/code>&nbsp;is&nbsp;<code>10.20.1.0\/24<\/code>;&nbsp;<code>subnet_pair<\/code>&nbsp;is&nbsp;<code>[\"10.20.0.0\/24\",\"10.20.1.0\/24\"]<\/code>; the digest and stable UUID do not change between runs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change the CIDR root from&nbsp;<code>10.20.0.0\/16<\/code>&nbsp;to&nbsp;<code>10.30.0.0\/16<\/code>. Compare&nbsp;<code>uuid()<\/code>&nbsp;in console with&nbsp;<code>uuidv5(\"dns\", \"demo.example.org\")<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 19 \u2014 First provider-defined function (built-in terraform provider)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-19-provider-defined-functions\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>provider::terraform::encode_tfvars()<\/code><br><strong>Objective:<\/strong>&nbsp;Call an actual provider-defined function without installing Azure or AWS providers.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Provider-defined functions use&nbsp;<code>provider::&lt;local-provider-name&gt;::&lt;function&gt;<\/code>. The built-in Terraform provider has&nbsp;<code>encode_tfvars<\/code>; its text formatting is not a public interchange format, so use&nbsp;<code>jsonencode<\/code>&nbsp;for JSON APIs instead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n  required_providers {\n    terraform = {\n      source = \"terraform.io\/builtin\/terraform\"\n    }\n  }\n}\n\nlocals {\n  settings = {\n    application = \"demo\"\n    replicas    = 2\n  }\n}\n\noutput \"tfvars_text\" { value = provider::terraform::encode_tfvars(local.settings) }\noutput \"json_text\"   { value = jsonencode(local.settings) }\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-19-provider-defined-functions init -backend=false\nterraform -chdir=labs\/lab-19-provider-defined-functions fmt\nterraform -chdir=labs\/lab-19-provider-defined-functions validate\nterraform -chdir=labs\/lab-19-provider-defined-functions plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-19-provider-defined-functions apply -auto-approve\nterraform -chdir=labs\/lab-19-provider-defined-functions output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The result is text with&nbsp;<code>application<\/code>&nbsp;and&nbsp;<code>replicas<\/code>&nbsp;assignments, compatible with&nbsp;<code>.tfvars<\/code>&nbsp;syntax.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Add&nbsp;<code>enabled = true<\/code>&nbsp;to the settings object. Compare&nbsp;<code>provider::terraform::encode_tfvars(...)<\/code>&nbsp;with&nbsp;<code>jsonencode(...)<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 20 \u2014 Azure Linux VM: calculate safe names and CIDRs before deployment<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-20-azure-vm-preview\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>lower(), trimspace(), replace(), format(), merge(), cidrsubnet(), lookup(), jsonencode()<\/code><br><strong>Objective:<\/strong>&nbsp;Prepare values for an Azure Ubuntu VM using only local expressions and outputs; zero Azure resources.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This preview is a safe preparation step for Lab 21. It is separate from the uploaded 13-resource variables lab so it cannot accidentally operate on the previous state. Keep the original names unchanged when adapting into an already-applied training environment.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n}\n\nvariable \"project\" {\n  type    = string\n  default = \" FuncDemo \"\n}\nvariable \"environment\" {\n  type    = string\n  default = \"dev\"\n}\n\nlocals {\n  prefix     = lower(replace(trimspace(var.project), \" \", \"-\"))\n  base_name  = format(\"%s-%s\", local.prefix, var.environment)\n  rg_name    = format(\"rg-%s\", local.base_name)\n  vm_name    = format(\"vm-%s\", local.base_name)\n  subnet     = cidrsubnet(\"10.70.0.0\/16\", 8, 1)\n  vm_size    = lookup({ dev = \"Standard_B1s\", prod = \"Standard_B2s_v2\" }, var.environment, \"Standard_B1s\")\n  tags       = merge({ managed_by = \"terraform\" }, { environment = var.environment })\n  config_json = jsonencode({ name = local.vm_name, subnet = local.subnet })\n}\n\noutput \"azure_preview\" {\n  value = {\n    resource_group = local.rg_name\n    vm_name        = local.vm_name\n    subnet         = local.subnet\n    vm_size        = local.vm_size\n    tags           = local.tags\n    config_json    = local.config_json\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform -chdir=labs\/lab-20-azure-vm-preview init -backend=false\nterraform -chdir=labs\/lab-20-azure-vm-preview fmt\nterraform -chdir=labs\/lab-20-azure-vm-preview validate\nterraform -chdir=labs\/lab-20-azure-vm-preview plan -input=false\n<em># The following creates NO cloud resources for this lab:<\/em>\nterraform -chdir=labs\/lab-20-azure-vm-preview apply -auto-approve\nterraform -chdir=labs\/lab-20-azure-vm-preview output\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>rg_name<\/code>&nbsp;is&nbsp;<code>rg-funcdemo-dev<\/code>,&nbsp;<code>vm_name<\/code>&nbsp;is&nbsp;<code>vm-funcdemo-dev<\/code>, and&nbsp;<code>subnet<\/code>&nbsp;is&nbsp;<code>10.70.1.0\/24<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change&nbsp;<code>environment<\/code>&nbsp;to&nbsp;<code>prod<\/code>&nbsp;and inspect how name, size, and tags change. Do not change existing deployed Azure names merely to add functions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 21 \u2014 Optional Azure Ubuntu VM: apply functions to minimal infrastructure<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-21-azure-linux-vm-deploy\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>lower(), replace(), format(), cidrsubnet(), merge(), file(), pathexpand(), templatefile(), base64encode()<\/code><br><strong>Objective:<\/strong>&nbsp;Create a resource group, VNet, subnet, NIC, and private Linux VM with generated cloud-init; then clean up.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A fully isolated, optional deployment lab: five Azure resources, SSH public-key authentication, no inbound public IP, no NSG, and cloud-init managed with&nbsp;<code>base64encode(templatefile(...))<\/code>. The VM name, CIDR and tags are derived by functions. Azure authentication\/subscription, region\/VM SKU\/image permissions, and costs are your responsibility. DO NOT copy this full root into the original 13-resource state directory.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n  required_providers {\n    azurerm = {\n      source  = \"hashicorp\/azurerm\"\n      version = \"~&gt; 5.9.0\"\n    }\n  }\n}\n\nprovider \"azurerm\" {\n  features {}\n}\n\nvariable \"region\" {\n  type    = string\n  default = \"eastus\"\n}\nvariable \"environment\" {\n  type    = string\n  default = \"dev\"\n}\nvariable \"ssh_public_key_path\" {\n  type    = string\n  default = \"~\/.ssh\/id_ed25519.pub\"\n}\n\nlocals {\n  name        = format(\"functions-%s\", lower(replace(var.environment, \" \", \"-\")))\n  subnet_cidr = cidrsubnet(\"10.70.0.0\/16\", 8, 1)\n  tags        = merge({ purpose = \"terraform-functions-lab\" }, { environment = var.environment })\n}\n\nresource \"azurerm_resource_group\" \"lab\" {\n  name     = \"rg-${local.name}\"\n  location = var.region\n  tags     = local.tags\n}\n\nresource \"azurerm_virtual_network\" \"lab\" {\n  name                = \"vnet-${local.name}\"\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n  address_space       = &#91;\"10.70.0.0\/16\"]\n}\n\nresource \"azurerm_subnet\" \"lab\" {\n  name                 = \"snet-linux\"\n  resource_group_name  = azurerm_resource_group.lab.name\n  virtual_network_name = azurerm_virtual_network.lab.name\n  address_prefixes     = &#91;local.subnet_cidr]\n}\n\nresource \"azurerm_network_interface\" \"lab\" {\n  name                = \"nic-${local.name}\"\n  location            = azurerm_resource_group.lab.location\n  resource_group_name = azurerm_resource_group.lab.name\n  ip_configuration {\n    name                          = \"internal\"\n    subnet_id                     = azurerm_subnet.lab.id\n    private_ip_address_allocation = \"Dynamic\"\n  }\n}\n\nresource \"azurerm_linux_virtual_machine\" \"lab\" {\n  name                  = \"vm-${local.name}\"\n  resource_group_name   = azurerm_resource_group.lab.name\n  location              = azurerm_resource_group.lab.location\n  size                  = \"Standard_B2s_v2\"\n  admin_username        = \"labadmin\"\n  network_interface_ids = &#91;azurerm_network_interface.lab.id]\n  disable_password_authentication = true\n  admin_ssh_key {\n    username   = \"labadmin\"\n    public_key = file(pathexpand(var.ssh_public_key_path))\n  }\n  os_disk {\n    caching              = \"ReadWrite\"\n    storage_account_type = \"Standard_LRS\"\n  }\n  source_image_reference {\n    publisher = \"Canonical\"\n    offer     = \"ubuntu-26_04-lts\"\n    sku       = \"server\"\n    version   = \"latest\"\n  }\n  custom_data = base64encode(templatefile(\"${path.module}\/cloud-init.tftpl\", {\n    environment = var.environment\n    vm_name     = \"vm-${local.name}\"\n    subnet      = local.subnet_cidr\n  }))\n  tags = local.tags\n}\n\noutput \"vm_name\"   { value = azurerm_linux_virtual_machine.lab.name }\noutput \"vm_id\"     { value = azurerm_linux_virtual_machine.lab.id }\noutput \"subnet\"    { value = local.subnet_cidr }\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>cloud-init.tftpl<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em>#cloud-config<\/em>\nwrite_files:\n  - path: \/etc\/functions-training.conf\n    permissions: '0644'\n    content: |\n      environment=${environment}\n      vm=${vm_name}\n      subnet=${subnet}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Run\u00a0<code>az login<\/code>, select\u00a0<strong>your intended subscription<\/strong>, and set\u00a0<code>ARM_SUBSCRIPTION_ID<\/code>.<\/li>\n\n\n\n<li>Ensure\u00a0<code>~\/.ssh\/id_ed25519.pub<\/code>\u00a0exists; otherwise create a fresh key using\u00a0<code>ssh-keygen -t ed25519 -f ~\/.ssh\/id_ed25519<\/code>\u00a0(do not overwrite an existing key).<\/li>\n\n\n\n<li>Use the dedicated\u00a0<code>lab-21<\/code>\u00a0directory. Do\u00a0<strong>not<\/strong>\u00a0share state with your earlier 13-resource lab.<\/li>\n\n\n\n<li>Run\u00a0<code>terraform init<\/code>,\u00a0<code>fmt<\/code>,\u00a0<code>validate<\/code>\u00a0and\u00a0<code>plan -out=lab.tfplan<\/code>.<\/li>\n\n\n\n<li>Review the\u00a0<strong>five creates<\/strong>, subscription and region. Only then apply\u00a0<code>lab.tfplan<\/code>.<\/li>\n\n\n\n<li>Verify with Azure Run Command (no public IP\/SSH connection needed), then\u00a0<code>terraform destroy<\/code>\u00a0when finished.<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>az login\naz account set --subscription \"YOUR_SUBSCRIPTION_ID\"\nexport ARM_SUBSCRIPTION_ID=\"$(az account show --query id -o tsv)\"\n<em># Create a key ONLY if you do not already have a suitable one:<\/em>\ntest -f ~\/.ssh\/id_ed25519.pub || ssh-keygen -t ed25519 -f ~\/.ssh\/id_ed25519\nterraform -chdir=labs\/lab-21-azure-linux-vm-deploy init\nterraform -chdir=labs\/lab-21-azure-linux-vm-deploy fmt\nterraform -chdir=labs\/lab-21-azure-linux-vm-deploy validate\nterraform -chdir=labs\/lab-21-azure-linux-vm-deploy plan -out=lab.tfplan\n<em># After manual review:<\/em>\nterraform -chdir=labs\/lab-21-azure-linux-vm-deploy apply lab.tfplan\naz vm run-command invoke --resource-group rg-functions-dev --name vm-functions-dev \\\n  --command-id RunShellScript --scripts 'cat \/etc\/functions-training.conf'\n<em># When finished:<\/em>\nterraform -chdir=labs\/lab-21-azure-linux-vm-deploy destroy\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If&nbsp;<code>az vm run-command<\/code>&nbsp;cannot yet read the file, allow cloud-init to finish and check its logs\/agent status. This lab provides no public IP; remote login requires your own approved private network access mechanism.&nbsp;<strong>Costs accrue for a running VM even if it has no public IP.<\/strong>&nbsp;On PowerShell, set&nbsp;<code>ARM_SUBSCRIPTION_ID<\/code>&nbsp;using&nbsp;<code>$env:ARM_SUBSCRIPTION_ID = ...<\/code>&nbsp;instead of&nbsp;<code>export<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>terraform plan<\/code>&nbsp;proposes five Azure resources in a NEW isolated state; the guest cloud-init writes&nbsp;<code>\/etc\/functions-training.conf<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Instructor note:<\/strong>&nbsp;Requires&nbsp;<code>az login<\/code>,&nbsp;<code>ARM_SUBSCRIPTION_ID<\/code>, a public SSH key,&nbsp;<code>terraform init<\/code>, and an explicit review before&nbsp;<code>terraform apply<\/code>. No public IP is provisioned.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change only the environment in the new lab and inspect whether Azure proposes replacement. Destroy the isolated lab when finished.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lab 22 \u2014 Optional AWS network lab: combine naming and CIDR functions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Folder:<\/strong>&nbsp;<code>labs\/lab-22-aws-vpc-subnet-deploy\/<\/code><br><strong>Functions:<\/strong>&nbsp;<code>lower(), replace(), format(), cidrsubnet(), merge()<\/code><br><strong>Objective:<\/strong>&nbsp;Create one VPC and one subnet with derived names and CIDR blocks, then destroy them.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Concept and scenario<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This example intentionally uses only two AWS resource types. AWS credentials, access rights, network quotas and region are required.&nbsp;<code>aws_subnet<\/code>&nbsp;is associated with the VPC by ID; it does not create an EC2 instance and there is no internet gateway or NAT gateway.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. Create the lab files (complete source code)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>main.tf<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>terraform {\n  required_version = \"&gt;= 1.15.0, &lt; 2.0.0\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp\/aws\"\n      version = \"~&gt; 6.0\"\n    }\n  }\n}\n\nvariable \"region\" {\n  type    = string\n  default = \"ap-northeast-1\"\n}\nvariable \"environment\" {\n  type    = string\n  default = \"dev\"\n}\nvariable \"network_cidr\" {\n  type    = string\n  default = \"10.80.0.0\/16\"\n}\n\nprovider \"aws\" { region = var.region }\n\nlocals {\n  prefix      = lower(replace(\"Functions Lab\", \" \", \"-\"))\n  name        = format(\"%s-%s\", local.prefix, var.environment)\n  subnet_cidr = cidrsubnet(var.network_cidr, 8, 1)\n  tags        = merge({ managed_by = \"terraform\" }, { environment = var.environment })\n}\n\nresource \"aws_vpc\" \"lab\" {\n  cidr_block = var.network_cidr\n  tags       = merge(local.tags, { Name = \"vpc-${local.name}\" })\n}\n\nresource \"aws_subnet\" \"lab\" {\n  vpc_id     = aws_vpc.lab.id\n  cidr_block = local.subnet_cidr\n  tags       = merge(local.tags, { Name = \"subnet-${local.name}\" })\n}\n\noutput \"vpc_id\"      { value = aws_vpc.lab.id }\noutput \"subnet_cidr\" { value = aws_subnet.lab.cidr_block }\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">C. Run step by step<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Configure AWS credentials through your approved IAM\/SSO mechanism and confirm the identity using\u00a0<code>aws sts get-caller-identity<\/code>.<\/li>\n\n\n\n<li>Run\u00a0<code>terraform init<\/code>,\u00a0<code>fmt<\/code>,\u00a0<code>validate<\/code>, and\u00a0<code>plan -out=lab.tfplan<\/code>\u00a0in the dedicated directory.<\/li>\n\n\n\n<li>Review that Terraform proposes only\u00a0<strong>one VPC and one subnet<\/strong>. Apply manually after approval and destroy afterwards.<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>aws sts get-caller-identity\nterraform -chdir=labs\/lab-22-aws-vpc-subnet-deploy init\nterraform -chdir=labs\/lab-22-aws-vpc-subnet-deploy fmt\nterraform -chdir=labs\/lab-22-aws-vpc-subnet-deploy validate\nterraform -chdir=labs\/lab-22-aws-vpc-subnet-deploy plan -out=lab.tfplan\n<em># After manual review:<\/em>\nterraform -chdir=labs\/lab-22-aws-vpc-subnet-deploy apply lab.tfplan\nterraform -chdir=labs\/lab-22-aws-vpc-subnet-deploy output\n<em># When finished:<\/em>\nterraform -chdir=labs\/lab-22-aws-vpc-subnet-deploy destroy\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Do not create this VPC in a production account. No NAT gateway, IGW, EC2 instance, or public subnet is created.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">D. Expected result \/ checks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>terraform plan<\/code>&nbsp;proposes one VPC and one subnet; the subnet CIDR is&nbsp;<code>10.80.1.0\/24<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Instructor note:<\/strong>&nbsp;Requires configured AWS credentials and permissions. Review&nbsp;<code>terraform plan<\/code>&nbsp;and apply only if permitted. Destroy the isolated network after the lab.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">E. Practice task<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change&nbsp;<code>environment<\/code>&nbsp;from&nbsp;<code>dev<\/code>&nbsp;to&nbsp;<code>stage<\/code>, inspect the tag changes, and then clean up using&nbsp;<code>terraform destroy<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Completion check:<\/strong>&nbsp;<code>terraform validate<\/code>&nbsp;passes (with provider installation where applicable); the calculated output matches the prediction; only explicit optional cloud labs can create infrastructure.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Function reference \u2014 quick syntax and use cases<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">These compact examples are intended for&nbsp;<code>terraform console<\/code>. Expressions that read files require those files to exist in the current lab.&nbsp;<code>plantimestamp()<\/code>&nbsp;is&nbsp;<strong>not<\/strong>&nbsp;a console function. Provider-defined functions require the provider declaration from Lab 19. The file examples are illustrative paths; use the actual fixture paths in the corresponding lab.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">String functions<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Function<\/th><th class=\"has-text-align-left\" data-align=\"left\">Example expression<\/th><th class=\"has-text-align-left\" data-align=\"left\">Why \/ important detail<\/th><\/tr><\/thead><tbody><tr><td><code>lower()<\/code><\/td><td><code>lower(\"DEV\")<\/code><\/td><td>Convert to lowercase.<\/td><\/tr><tr><td><code>upper()<\/code><\/td><td><code>upper(\"dev\")<\/code><\/td><td>Convert to uppercase.<\/td><\/tr><tr><td><code>title()<\/code><\/td><td><code>title(\"terraform functions\")<\/code><\/td><td>Title-style casing.<\/td><\/tr><tr><td><code>trim()<\/code><\/td><td><code>trim(\"--dev--\", \"-\")<\/code><\/td><td>Trim a set of edge characters.<\/td><\/tr><tr><td><code>trimspace()<\/code><\/td><td><code>trimspace(\" dev \")<\/code><\/td><td>Trim surrounding whitespace.<\/td><\/tr><tr><td><code>chomp()<\/code><\/td><td><code>chomp(\"hello\\n\")<\/code><\/td><td>Remove trailing newline characters.<\/td><\/tr><tr><td><code>replace()<\/code><\/td><td><code>replace(\"my vm\", \" \", \"-\")<\/code><\/td><td>Replace substring or regex matches.<\/td><\/tr><tr><td><code>substr()<\/code><\/td><td><code>substr(\"terraform\", 0, 5)<\/code><\/td><td>Select a substring using offset and length.<\/td><\/tr><tr><td><code>split()<\/code><\/td><td><code>split(\",\", \"dev,prod\")<\/code><\/td><td>Separate string into list.<\/td><\/tr><tr><td><code>join()<\/code><\/td><td><code>join(\"-\", [\"vm\", \"dev\"])<\/code><\/td><td>Join list elements.<\/td><\/tr><tr><td><code>startswith()<\/code><\/td><td><code>startswith(\"vm-dev\", \"vm-\")<\/code><\/td><td>Test prefix.<\/td><\/tr><tr><td><code>endswith()<\/code><\/td><td><code>endswith(\"vm-dev\", \"-dev\")<\/code><\/td><td>Test suffix.<\/td><\/tr><tr><td><code>format()<\/code><\/td><td><code>format(\"vm-%02d\", 3)<\/code><\/td><td>Format one string.<\/td><\/tr><tr><td><code>formatlist()<\/code><\/td><td><code>formatlist(\"vm-%02d\", [1,2])<\/code><\/td><td>Format a list of strings.<\/td><\/tr><tr><td><code>regex()<\/code><\/td><td><code>regex(\"[a-z]+\", \"vm12\")<\/code><\/td><td>One regex match; error on no match.<\/td><\/tr><tr><td><code>regexall()<\/code><\/td><td><code>regexall(\"[0-9]+\", \"a1b2\")<\/code><\/td><td>Every regex match, or empty list.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Collection functions<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Function<\/th><th class=\"has-text-align-left\" data-align=\"left\">Example expression<\/th><th class=\"has-text-align-left\" data-align=\"left\">Why \/ important detail<\/th><\/tr><\/thead><tbody><tr><td><code>length()<\/code><\/td><td><code>length([\"dev\", \"prod\"])<\/code><\/td><td>Length of list\/map\/string.<\/td><\/tr><tr><td><code>element()<\/code><\/td><td><code>element([\"a\",\"b\"], 3)<\/code><\/td><td>Wrap-around index.<\/td><\/tr><tr><td><code>concat()<\/code><\/td><td><code>concat([\"a\"], [\"b\"])<\/code><\/td><td>Concatenate lists.<\/td><\/tr><tr><td><code>flatten()<\/code><\/td><td><code>flatten([[\"a\"],[\"b\"]])<\/code><\/td><td>Flatten nested lists.<\/td><\/tr><tr><td><code>distinct()<\/code><\/td><td><code>distinct([\"a\",\"a\",\"b\"])<\/code><\/td><td>Remove duplicate values.<\/td><\/tr><tr><td><code>compact()<\/code><\/td><td><code>compact([\"a\",\"\",null])<\/code><\/td><td>Remove empty string and null elements.<\/td><\/tr><tr><td><code>contains()<\/code><\/td><td><code>contains([\"dev\"], \"dev\")<\/code><\/td><td>Membership check.<\/td><\/tr><tr><td><code>index()<\/code><\/td><td><code>index([\"a\",\"b\"], \"b\")<\/code><\/td><td>Index of value; error when missing.<\/td><\/tr><tr><td><code>tolist()<\/code><\/td><td><code>tolist(toset([\"a\",\"b\"]))<\/code><\/td><td>Normalize to list.<\/td><\/tr><tr><td><code>toset()<\/code><\/td><td><code>toset([\"a\",\"a\",\"b\"])<\/code><\/td><td>Normalize to set (unique).<\/td><\/tr><tr><td><code>tomap()<\/code><\/td><td><code>tomap({ env = \"dev\" })<\/code><\/td><td>Normalize to map.<\/td><\/tr><tr><td><code>keys()<\/code><\/td><td><code>keys({ b = 2, a = 1 })<\/code><\/td><td>Map keys, lexically sorted.<\/td><\/tr><tr><td><code>values()<\/code><\/td><td><code>values({ b = 2, a = 1 })<\/code><\/td><td>Map values ordered by sorted keys.<\/td><\/tr><tr><td><code>lookup()<\/code><\/td><td><code>lookup({ dev = \"small\" }, \"stage\", \"small\")<\/code><\/td><td>Read map value with default.<\/td><\/tr><tr><td><code>merge()<\/code><\/td><td><code>merge({a=1},{a=2})<\/code><\/td><td>Later maps override matching keys.<\/td><\/tr><tr><td><code>zipmap()<\/code><\/td><td><code>zipmap([\"a\"],[1])<\/code><\/td><td>Build map from parallel lists.<\/td><\/tr><tr><td><code>matchkeys()<\/code><\/td><td><code>matchkeys([\"vm1\",\"vm2\"],[\"dev\",\"prod\"],[\"dev\"])<\/code><\/td><td>Filter parallel lists by key values.<\/td><\/tr><tr><td><code>sort()<\/code><\/td><td><code>sort([\"prod\", \"dev\"])<\/code><\/td><td>Sort list of strings.<\/td><\/tr><tr><td><code>reverse()<\/code><\/td><td><code>reverse([\"a\",\"b\"])<\/code><\/td><td>Reverse list.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Numeric and mathematical functions<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Function<\/th><th class=\"has-text-align-left\" data-align=\"left\">Example expression<\/th><th class=\"has-text-align-left\" data-align=\"left\">Why \/ important detail<\/th><\/tr><\/thead><tbody><tr><td><code>abs()<\/code><\/td><td><code>abs(-3)<\/code><\/td><td>Absolute value.<\/td><\/tr><tr><td><code>ceil()<\/code><\/td><td><code>ceil(2.1)<\/code><\/td><td>Round up.<\/td><\/tr><tr><td><code>floor()<\/code><\/td><td><code>floor(2.9)<\/code><\/td><td>Round down.<\/td><\/tr><tr><td><code>max()<\/code><\/td><td><code>max(1,3,2)<\/code><\/td><td>Largest number.<\/td><\/tr><tr><td><code>min()<\/code><\/td><td><code>min(1,3,2)<\/code><\/td><td>Smallest number.<\/td><\/tr><tr><td><code>parseint()<\/code><\/td><td><code>parseint(\"ff\", 16)<\/code><\/td><td>Parse integer in a radix.<\/td><\/tr><tr><td><code>signum()<\/code><\/td><td><code>signum(-5)<\/code><\/td><td>-1, 0 or 1.<\/td><\/tr><tr><td><code>log()<\/code><\/td><td><code>log(100, 10)<\/code><\/td><td>Logarithm with base.<\/td><\/tr><tr><td><code>pow()<\/code><\/td><td><code>pow(2, 3)<\/code><\/td><td>Exponentiation.<\/td><\/tr><tr><td><code>sum()<\/code><\/td><td><code>sum([1,2,3])<\/code><\/td><td>Sum collection values.<\/td><\/tr><tr><td><code>range()<\/code><\/td><td><code>range(1, 6, 2)<\/code><\/td><td>List: start inclusive, limit exclusive.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Conversion, validation and error functions<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Function<\/th><th class=\"has-text-align-left\" data-align=\"left\">Example expression<\/th><th class=\"has-text-align-left\" data-align=\"left\">Why \/ important detail<\/th><\/tr><\/thead><tbody><tr><td><code>tostring()<\/code><\/td><td><code>tostring(123)<\/code><\/td><td>Convert to string.<\/td><\/tr><tr><td><code>tonumber()<\/code><\/td><td><code>tonumber(\"123\")<\/code><\/td><td>Convert to number.<\/td><\/tr><tr><td><code>tobool()<\/code><\/td><td><code>tobool(\"true\")<\/code><\/td><td>Convert &#8220;true&#8221; or &#8220;false&#8221; to boolean.<\/td><\/tr><tr><td><code>try()<\/code><\/td><td><code>try({}.missing, \"default\")<\/code><\/td><td>Return first successful expression.<\/td><\/tr><tr><td><code>can()<\/code><\/td><td><code>can(tonumber(\"abc\"))<\/code><\/td><td>Can evaluate expression without error?<\/td><\/tr><tr><td><code>coalesce()<\/code><\/td><td><code>coalesce(null, \"\", \"dev\")<\/code><\/td><td>First non-null, nonempty string.<\/td><\/tr><tr><td><code>coalescelist()<\/code><\/td><td><code>coalescelist([], [\"dev\"])<\/code><\/td><td>First nonempty list.<\/td><\/tr><tr><td><code>one()<\/code><\/td><td><code>one([\"only\"])<\/code><\/td><td>Only or zero collection element.<\/td><\/tr><tr><td><code>sensitive()<\/code><\/td><td><code>sensitive(\"demo\")<\/code><\/td><td>Mark a value as sensitive.<\/td><\/tr><tr><td><code>nonsensitive()<\/code><\/td><td><code>nonsensitive(sensitive(\"public\"))<\/code><\/td><td>Unmark only known-safe values.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">File, encoding and data functions<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Function<\/th><th class=\"has-text-align-left\" data-align=\"left\">Example expression<\/th><th class=\"has-text-align-left\" data-align=\"left\">Why \/ important detail<\/th><\/tr><\/thead><tbody><tr><td><code>file()<\/code><\/td><td><code>file(\"${path.module}\/files\/file.txt\")<\/code><\/td><td>Read UTF-8 text from existing file.<\/td><\/tr><tr><td><code>fileexists()<\/code><\/td><td><code>fileexists(\"${path.module}\/files\/file.txt\")<\/code><\/td><td>Check file existence.<\/td><\/tr><tr><td><code>templatefile()<\/code><\/td><td><code>templatefile(\"${path.module}\/file.tftpl\", { name = \"web\" })<\/code><\/td><td>Render existing template with vars.<\/td><\/tr><tr><td><code>jsonencode()<\/code><\/td><td><code>jsonencode({ name = \"web\" })<\/code><\/td><td>Terraform value to JSON text.<\/td><\/tr><tr><td><code>jsondecode()<\/code><\/td><td><code>jsondecode(\"{\\\"name\\\":\\\"web\\\"}\")<\/code><\/td><td>JSON text to Terraform value.<\/td><\/tr><tr><td><code>yamlencode()<\/code><\/td><td><code>yamlencode({ name = \"web\" })<\/code><\/td><td>Terraform value to YAML text.<\/td><\/tr><tr><td><code>yamldecode()<\/code><\/td><td><code>yamldecode(\"name: web\")<\/code><\/td><td>YAML text to Terraform value.<\/td><\/tr><tr><td><code>base64encode()<\/code><\/td><td><code>base64encode(\"hi\")<\/code><\/td><td>UTF-8 text to Base64.<\/td><\/tr><tr><td><code>base64decode()<\/code><\/td><td><code>base64decode(\"aGk=\")<\/code><\/td><td>Base64 into UTF-8 string.<\/td><\/tr><tr><td><code>filebase64()<\/code><\/td><td><code>filebase64(\"${path.module}\/files\/file.txt\")<\/code><\/td><td>Existing file bytes to Base64.<\/td><\/tr><tr><td><code>textdecodebase64()<\/code><\/td><td><code>textdecodebase64(\"aGk=\", \"UTF-8\")<\/code><\/td><td>Base64 decoded with given charset.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Filesystem, path and time functions<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Function<\/th><th class=\"has-text-align-left\" data-align=\"left\">Example expression<\/th><th class=\"has-text-align-left\" data-align=\"left\">Why \/ important detail<\/th><\/tr><\/thead><tbody><tr><td><code>abspath()<\/code><\/td><td><code>abspath(\".\/files\")<\/code><\/td><td>Convert to absolute path.<\/td><\/tr><tr><td><code>dirname()<\/code><\/td><td><code>dirname(\"\/tmp\/example.txt\")<\/code><\/td><td>Return parent directory.<\/td><\/tr><tr><td><code>basename()<\/code><\/td><td><code>basename(\"\/tmp\/example.txt\")<\/code><\/td><td>Return last component.<\/td><\/tr><tr><td><code>pathexpand()<\/code><\/td><td><code>pathexpand(\"~\/.ssh\/id_ed25519.pub\")<\/code><\/td><td>Expand home directory.<\/td><\/tr><tr><td><code>timestamp()<\/code><\/td><td><code>timestamp()<\/code><\/td><td>Apply-time timestamp, changes between applies.<\/td><\/tr><tr><td><code>timeadd()<\/code><\/td><td><code>timeadd(\"2026-10-14T00:00:00Z\", \"24h\")<\/code><\/td><td>Add duration.<\/td><\/tr><tr><td><code>plantimestamp()<\/code><\/td><td><code>plantimestamp()<\/code><\/td><td>Plan timestamp; not in console.<\/td><\/tr><tr><td><code>formatdate()<\/code><\/td><td><code>formatdate(\"YYYY-MM-DD\", \"2026-10-14T00:00:00Z\")<\/code><\/td><td>Format date with Terraform tokens.<\/td><\/tr><tr><td><code>fileset()<\/code><\/td><td><code>fileset(\"${path.module}\/files\", \"*.yaml\")<\/code><\/td><td>Find filenames by glob.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Advanced functions<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Function<\/th><th class=\"has-text-align-left\" data-align=\"left\">Example expression<\/th><th class=\"has-text-align-left\" data-align=\"left\">Why \/ important detail<\/th><\/tr><\/thead><tbody><tr><td><code>cidrsubnet()<\/code><\/td><td><code>cidrsubnet(\"10.20.0.0\/16\", 8, 1)<\/code><\/td><td>Select one new subnet.<\/td><\/tr><tr><td><code>cidrsubnets()<\/code><\/td><td><code>cidrsubnets(\"10.20.0.0\/16\", 8, 8)<\/code><\/td><td>Allocate consecutive subnet ranges.<\/td><\/tr><tr><td><code>md5()<\/code><\/td><td><code>md5(\"hello\")<\/code><\/td><td>MD5 digest of text.<\/td><\/tr><tr><td><code>sha256()<\/code><\/td><td><code>sha256(\"hello\")<\/code><\/td><td>SHA-256 digest of text.<\/td><\/tr><tr><td><code>filemd5()<\/code><\/td><td><code>filemd5(\"${path.module}\/files\/release.txt\")<\/code><\/td><td>MD5 of file bytes.<\/td><\/tr><tr><td><code>uuid()<\/code><\/td><td><code>uuid()<\/code><\/td><td>Random UUID, not deterministic.<\/td><\/tr><tr><td><code>uuidv5()<\/code><\/td><td><code>uuidv5(\"dns\", \"app.example.org\")<\/code><\/td><td>Stable name-based UUID.<\/td><\/tr><tr><td><code>provider::terraform::encode_tfvars()<\/code><\/td><td><code>provider::terraform::encode_tfvars({ name = \"web\" })<\/code><\/td><td>Built-in provider-defined function; requires provider declaration.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Path symbols are values, not function calls<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Value<\/th><th class=\"has-text-align-left\" data-align=\"left\">Meaning<\/th><th class=\"has-text-align-left\" data-align=\"left\">Example<\/th><\/tr><\/thead><tbody><tr><td><code>path.module<\/code><\/td><td>Directory of the module whose expression is being evaluated<\/td><td><code>file(\"${path.module}\/files\/config.json\")<\/code><\/td><\/tr><tr><td><code>path.root<\/code><\/td><td>Root module directory<\/td><td><code>${path.root}\/templates<\/code><\/td><\/tr><tr><td><code>path.cwd<\/code><\/td><td>Working directory in which Terraform was started (before&nbsp;<code>-chdir<\/code>)<\/td><td>Diagnostic use only<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">When to use functions in production<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Task<\/th><th class=\"has-text-align-left\" data-align=\"left\">Preferred approach<\/th><th class=\"has-text-align-left\" data-align=\"left\">Caution<\/th><\/tr><\/thead><tbody><tr><td>Make a readable VM name<\/td><td><code>format<\/code>&nbsp;+&nbsp;<code>lower<\/code>&nbsp;+&nbsp;<code>replace<\/code><\/td><td>Cloud provider name length\/character restrictions still apply<\/td><\/tr><tr><td>Avoid blank collection values<\/td><td><code>compact<\/code>,&nbsp;<code>distinct<\/code>,&nbsp;<code>sort<\/code><\/td><td>Set iteration order cannot be relied on<\/td><\/tr><tr><td>Choose a default VM size<\/td><td><code>lookup<\/code>&nbsp;with an explicit default<\/td><td>Validate every permitted SKU\/region<\/td><\/tr><tr><td>Calculate subnets<\/td><td><code>cidrsubnet<\/code>,&nbsp;<code>cidrsubnets<\/code><\/td><td>Don&#8217;t change assigned CIDR positions in production<\/td><\/tr><tr><td>Render cloud-init<\/td><td><code>templatefile<\/code>&nbsp;and (for AzureRM Linux VM)&nbsp;<code>base64encode<\/code><\/td><td>Keep secrets out of templates\/state<\/td><\/tr><tr><td>Read JSON or YAML config<\/td><td><code>jsondecode(file(...))<\/code>,&nbsp;<code>yamldecode(file(...))<\/code><\/td><td>Only pre-existing UTF-8 files<\/td><\/tr><tr><td>Mask a CLI output<\/td><td><code>sensitive<\/code><\/td><td>Still visible in state to authorized readers<\/td><\/tr><tr><td>Default missing optional data<\/td><td><code>try<\/code>,&nbsp;<code>can<\/code>,&nbsp;<code>coalesce<\/code><\/td><td>Don&#8217;t hide bad schema errors with blanket fallbacks<\/td><\/tr><tr><td>Create a stable identifier<\/td><td><code>uuidv5<\/code><\/td><td>Do not use&nbsp;<code>uuid<\/code>&nbsp;for resource name stability<\/td><\/tr><tr><td>Mark the current run time<\/td><td>A purpose-built time provider or external event<\/td><td><code>timestamp<\/code>&nbsp;creates perpetual drift when attached to resource attributes<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h1 class=\"wp-block-heading\">Troubleshooting and common mistakes<\/h1>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Symptom<\/th><th class=\"has-text-align-left\" data-align=\"left\">Cause<\/th><th class=\"has-text-align-left\" data-align=\"left\">Fix<\/th><\/tr><\/thead><tbody><tr><td><code>Invalid function argument<\/code>&nbsp;for&nbsp;<code>tobool(\"yes\")<\/code><\/td><td>Only&nbsp;<code>\"true\"<\/code>,&nbsp;<code>\"false\"<\/code>, bool and null are convertible<\/td><td>Normalize at input boundary or reject the input<\/td><\/tr><tr><td><code>regex<\/code>&nbsp;fails for an unmatched pattern<\/td><td><code>regex<\/code>&nbsp;does not return null<\/td><td>Use&nbsp;<code>regexall<\/code>&nbsp;for zero-or-more matches or&nbsp;<code>can(regex(...))<\/code><\/td><\/tr><tr><td><code>index<\/code>&nbsp;fails<\/td><td>Item does not exist<\/td><td>Check with&nbsp;<code>contains<\/code>&nbsp;or use&nbsp;<code>lookup<\/code>&nbsp;for maps<\/td><\/tr><tr><td><code>one<\/code>&nbsp;fails<\/td><td>Collection has more than one member<\/td><td>Make selection rules explicit<\/td><\/tr><tr><td><code>element<\/code>&nbsp;unexpectedly returns first item<\/td><td>Intentional modulo wrap-around<\/td><td>Use&nbsp;<code>list[index]<\/code>&nbsp;for strict indexing<\/td><\/tr><tr><td><code>toset<\/code>&nbsp;seems to reorder output<\/td><td>Sets have no guaranteed order<\/td><td><code>sort(tolist(toset(values)))<\/code><\/td><\/tr><tr><td><code>merge<\/code>&nbsp;overwrites a tag<\/td><td>Rightmost map wins<\/td><td>Put overrides intentionally last<\/td><\/tr><tr><td><code>file()<\/code>&nbsp;cannot find a file<\/td><td>Relative to wrong location or not created yet<\/td><td><code>${path.module}\/files\/file.txt<\/code>; only static files<\/td><\/tr><tr><td><code>file()<\/code>&nbsp;reports invalid UTF-8<\/td><td>Binary data is not UTF-8<\/td><td>Use&nbsp;<code>filebase64<\/code>&nbsp;for bytes<\/td><\/tr><tr><td><code>templatefile()<\/code>&nbsp;reports missing key<\/td><td><code>vars<\/code>&nbsp;object omits an interpolated name<\/td><td>Pass the key in the object<\/td><\/tr><tr><td>JSON is malformed after concatenation<\/td><td>Manual string building<\/td><td>Use&nbsp;<code>jsonencode<\/code><\/td><\/tr><tr><td>Azure&nbsp;<code>custom_data<\/code>&nbsp;is invalid<\/td><td>Unencoded user data on newer VM resource<\/td><td><code>base64encode(templatefile(...))<\/code><\/td><\/tr><tr><td><code>plantimestamp<\/code>&nbsp;fails in console<\/td><td>Function is plan-only<\/td><td>Run&nbsp;<code>terraform plan<\/code><\/td><\/tr><tr><td>Diff on each plan after using&nbsp;<code>timestamp<\/code><\/td><td>Time changes every run<\/td><td>Remove from stable resource attributes<\/td><\/tr><tr><td><code>nonsensitive<\/code>&nbsp;leaks a value<\/td><td>Sensitive mark deliberately removed<\/td><td>Use only when the derived value is demonstrably public<\/td><\/tr><tr><td>VM replacement after refactoring expressions<\/td><td>Renamed a resource or changed a ForceNew property<\/td><td>Revert the change; inspect&nbsp;<code>terraform plan<\/code><\/td><\/tr><tr><td>Azure provider registration\/quota failure<\/td><td>Subscription is not configured for chosen region\/SKU<\/td><td>Check resource provider, quota and image availability<\/td><\/tr><tr><td>Azure private VM cannot accept SSH from laptop<\/td><td>No public IP or reachable private route<\/td><td>Use approved VPN\/bastion or&nbsp;<code>az vm run-command<\/code><\/td><\/tr><tr><td>Provider&nbsp;<code>init<\/code>&nbsp;cannot download<\/td><td>Internet\/proxy\/registry limitations<\/td><td>Verify connectivity, mirror and provider version; local-only labs don&#8217;t need cloud providers<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h1 class=\"wp-block-heading\">Instructor-led exercises and assessment<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Assignment 1 \u2014 Standardize infrastructure names (beginner).<\/strong>&nbsp;Starting from Lab 04, add a new input variable&nbsp;<code>team<\/code>&nbsp;with default&nbsp;<code>Platform Team<\/code>. Derive a label containing project, team and environment in lowercase with hyphens. Use only&nbsp;<code>lower<\/code>,&nbsp;<code>trimspace<\/code>,&nbsp;<code>replace<\/code>,&nbsp;<code>join<\/code>&nbsp;and&nbsp;<code>format<\/code>. No resources. Success: a reproducible name and a readable output; see&nbsp;<code>assessments\/solution-01\/main.tf<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Assignment 2 \u2014 Generate a safe deployment preview (intermediate).<\/strong>&nbsp;Using Lab 20 as a reference, derive an Azure resource group name, two non-overlapping \/24 CIDRs from a \/16, a stable tag map and a JSON representation of that map. Use&nbsp;<code>cidrsubnets<\/code>,&nbsp;<code>merge<\/code>,&nbsp;<code>lower<\/code>,&nbsp;<code>replace<\/code>,&nbsp;<code>jsonencode<\/code>. No Azure deployment. Success:&nbsp;<code>terraform validate<\/code>&nbsp;and a plan displaying both CIDRs; see&nbsp;<code>assessments\/solution-02\/main.tf<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Assignment 3 \u2014 Explain, don&#8217;t just run (discussion).<\/strong>&nbsp;Why are these patterns risky?&nbsp;<code>uuid()<\/code>&nbsp;in a resource name;&nbsp;<code>timestamp()<\/code>&nbsp;in tags;&nbsp;<code>nonsensitive(var.api_key)<\/code>;&nbsp;<code>file(\".\/build\/generated.json\")<\/code>&nbsp;where the file is produced by a Terraform resource; and&nbsp;<code>tolist(toset(...))<\/code>&nbsp;used for subnet ordering.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Knowledge check and answer key<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Can Terraform functions create resources?<\/strong>\u00a0No; they transform or read values used elsewhere in configuration.<\/li>\n\n\n\n<li><strong>What does\u00a0<code>try<\/code>\u00a0return?<\/strong>\u00a0The first expression that does not raise a dynamic evaluation error.<\/li>\n\n\n\n<li><strong>Does\u00a0<code>sensitive<\/code>\u00a0encrypt state?<\/strong>\u00a0No; it controls Terraform&#8217;s handling\/display of the sensitivity mark.<\/li>\n\n\n\n<li><strong>Why prefer\u00a0<code>jsonencode<\/code>\u00a0over manually assembled JSON text?<\/strong>\u00a0It correctly serializes nested data, quotes and escaping.<\/li>\n\n\n\n<li><strong>How do\u00a0<code>timestamp<\/code>\u00a0and\u00a0<code>plantimestamp<\/code>\u00a0differ?<\/strong>\u00a0Apply-time unpredictable result vs. plan-time fixed result; both vary across runs, and\u00a0<code>plantimestamp<\/code>\u00a0cannot be evaluated in the console.<\/li>\n\n\n\n<li><strong>Can you reliably read a file created by another resource during the same apply using\u00a0<code>file()<\/code>?<\/strong>\u00a0No.<\/li>\n\n\n\n<li><strong>Why does\u00a0<code>sort(tolist(toset(...)))<\/code>\u00a0matter?<\/strong>\u00a0It removes duplicates and guarantees sorted list output, rather than depending on set iteration order.<\/li>\n\n\n\n<li><strong>When to use\u00a0<code>uuidv5<\/code>?<\/strong>\u00a0When a repeatable ID is needed from the same namespace and name.<\/li>\n\n\n\n<li><strong>How should students run optional cloud labs?<\/strong>\u00a0Authenticate, use a separate state, run\u00a0<code>init<\/code>\/<code>validate<\/code>, carefully review the plan, apply deliberately, verify, then destroy.<\/li>\n\n\n\n<li><strong>What is the safest way to introduce new function outputs into the uploaded Azure variables lab?<\/strong>\u00a0Add\u00a0<code>locals<\/code>\u00a0and\u00a0<code>output<\/code>\u00a0only, preserve existing resource names\/values\/state, and review for unexpected changes.<\/li>\n<\/ol>\n\n\n\n<h1 class=\"wp-block-heading\">Validation and cleanup<\/h1>\n\n\n\n<pre class=\"wp-block-code\"><code><em># Check local labs (requires Terraform CLI; never performs an apply)<\/em>\nbash tools\/validate_labs.sh\n\n<em># Optional: include AWS\/Azure providers (provider downloads required)<\/em>\nbash tools\/validate_labs.sh --include-cloud\n\n<em># Destroy the optional cloud lessons **only if you created them**<\/em>\nterraform -chdir=labs\/lab-21-azure-linux-vm-deploy destroy\nterraform -chdir=labs\/lab-22-aws-vpc-subnet-deploy destroy\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do not run destroy in the old Azure lab folder<\/strong>: its original 13 resources and state are unrelated to these independent examples. State and saved Terraform plan files may contain sensitive values; don&#8217;t commit or share them. These files are training examples and cloud deployment results depend on subscription, region, quotas, provider versions and permissions.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Official reference documentation<\/h1>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/functions\">Terraform built-in functions<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/expressions\/function-calls\">Terraform function calls and evaluation<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/cli\/commands\/console\">Terraform console<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/functions\/try\">Terraform type conversion and error functions<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/functions\/terraform-encode_tfvars\">Terraform provider-defined functions<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/functions\/plantimestamp\">Terraform\u00a0<code>plantimestamp<\/code><\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/functions\/cidrsubnets\">Terraform\u00a0<code>cidrsubnets<\/code><\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/functions\/file\">Terraform\u00a0<code>file<\/code><\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/functions\/templatefile\">Terraform\u00a0<code>templatefile<\/code><\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/developer.hashicorp.com\/terraform\/language\/functions\/nonsensitive\">Terraform\u00a0<code>nonsensitive<\/code><\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/virtual-machines\/linux\/create-ssh-keys-detailed\">Azure Linux VM SSH keys<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/registry.terraform.io\/providers\/hashicorp\/azurerm\/latest\/docs\/resources\/linux_virtual_machine\">AzureRM Linux VM resource<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/registry.terraform.io\/providers\/hashicorp\/aws\/latest\/docs\">AWS provider documentation<\/a><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Last reviewed for this course: 14 October 2026. Actual cloud applies were not performed while generating the course.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Beginner to Intermediate \u00b7 8 modules \u00b7 22 progressively designed labs \u00b7 20 local-only labs + 2 optional cloud labs \u00b7 Updated 14 October 2026 Purpose:&nbsp;learn Terraform&#8230; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1239","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/posts\/1239","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/comments?post=1239"}],"version-history":[{"count":1,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/posts\/1239\/revisions"}],"predecessor-version":[{"id":1240,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/posts\/1239\/revisions\/1240"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/media?parent=1239"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/categories?post=1239"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/tutorials\/wp-json\/wp\/v2\/tags?post=1239"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}