<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ozan Guner</title>
    <description>The latest articles on DEV Community by Ozan Guner (@ozanguner).</description>
    <link>https://dev.to/ozanguner</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F875454%2Fd3457342-e026-4704-bd3c-1c7b815dc818.jpg</url>
      <title>DEV Community: Ozan Guner</title>
      <link>https://dev.to/ozanguner</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ozanguner"/>
    <language>en</language>
    <item>
      <title>Terraform Basics – Week 6: Building and Using Your First Terraform Module</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Sun, 07 Dec 2025 21:41:29 +0000</pubDate>
      <link>https://dev.to/ozanguner/terraform-basics-week-6-building-and-using-your-first-terraform-module-3k9m</link>
      <guid>https://dev.to/ozanguner/terraform-basics-week-6-building-and-using-your-first-terraform-module-3k9m</guid>
      <description>&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Recap of Previous Weeks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. What are Terraform Modules and Why Do We Use Modules in Terraform?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Understanding Module Inputs and Outputs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Creating Our First Terraform Module&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Using the Module in Our Root Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Deploying to Azure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Recap of Previous Weeks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Over the past five weeks, we built a small but realistic Azure environment while learning the core Terraform concepts. We started by deploying our first VM, then introduced variables and tfvars files to make the configuration more flexible. We added security with NSGs and dynamic blocks, and finally exposed useful information through output values.&lt;/p&gt;

&lt;p&gt;Here's the visual representation of the infrastructure we have so far:&lt;/p&gt;

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

&lt;p&gt;Here's what .tf files we have under our Project Folder in Visual Studio Code:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;nsg.tf&lt;/strong&gt; &amp;gt; Contains the Network Security Group and the NSG association.&lt;br&gt;
&lt;strong&gt;outputs.tf&lt;/strong&gt; &amp;gt; Contains the output values.&lt;br&gt;
&lt;strong&gt;providers.tf&lt;/strong&gt; &amp;gt; Contains the provider block (AzureRM)&lt;br&gt;
&lt;strong&gt;resource-group.tf&lt;/strong&gt; &amp;gt; Contains the resource group resource block.&lt;br&gt;
&lt;strong&gt;terraform.tfvars&lt;/strong&gt; &amp;gt; Contains the values for our variables for the project.&lt;br&gt;
&lt;strong&gt;variables.tf&lt;/strong&gt; &amp;gt; Contains the definition of all variables we used for our project.&lt;br&gt;
&lt;strong&gt;virtual-machine.tf&lt;/strong&gt; &amp;gt; Contains the virtual machine and Network Interface Card (NIC) resources.&lt;br&gt;
&lt;strong&gt;virtual-network.tf&lt;/strong&gt; &amp;gt; Contains the Virtual Network and subnet resources.&lt;/p&gt;

&lt;p&gt;At this point, we have a fully working VM deployment that is parameterized, secure and able to surface important data. As the configuration grows, we are beginning to see repeated patterns, and managing multiple similar resources would quickly become difficult.&lt;/p&gt;

&lt;p&gt;This is the perfect time to introduce Terraform modules.&lt;/p&gt;

&lt;p&gt;If you missed the previous week's posts and want to do a deeper dive, here are the links to them :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/ozanguner/terraform-basics-week-1-deploying-your-first-azure-vm-1f86"&gt;Week 1&lt;/a&gt;&lt;br&gt;
 &lt;a href="https://dev.to/ozanguner/terraform-basics-week-2-variables-and-reusability-2c15"&gt;Week 2 &lt;/a&gt;&lt;br&gt;
 &lt;a href="https://dev.to/ozanguner/terraform-basics-week-3-managing-variables-with-tfvars-files-j8i"&gt;Week 3&lt;/a&gt; &lt;br&gt;
 &lt;a href="https://dev.to/ozanguner/terraform-basics-week-4-securing-your-azure-vms-with-nsgs-and-dynamic-blocks-3mh0"&gt;Week 4&lt;/a&gt;&lt;br&gt;
 &lt;a href="https://dev.to/ozanguner/terraform-basics-week-5-exposing-infrastructure-data-with-outputs-5h6n"&gt;Week 5&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/OzangunerGH/Terraform-Basics-Series" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; repository link for this series, where you can find the full .tf files used for every week including this week.&lt;/p&gt;

&lt;p&gt;Now let's get started with Terraform Modules!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. What are Terraform Modules and Why Do We Use Modules in Terraform?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Terraform module is simply a collection of resources grouped together so they can be reused as a single unit. Instead of rewriting the same VM, NIC or NSG blocks every time we deploy a new virtual machine, we define them once inside a module and call that module whenever we need another instance.&lt;/p&gt;

&lt;p&gt;Before we continue, it helps to understand how Terraform organizes configuration. The folder where your main Terraform files live is called the root module. This is where Terraform starts. Any module you place in a subfolder becomes a child module, and it cannot run on its own. The root module provides the inputs, and the child module returns outputs. This relationship is what allows us to reuse infrastructure patterns cleanly without duplicating code.&lt;/p&gt;

&lt;p&gt;Modules become especially important in real-world environments. Imagine a team managing ten VMs across development, staging and production. Without modules, each environment ends up with its own copy of the same resource blocks. A simple change—like updating tags or adjusting an NSG rule—must now be repeated in multiple places. Over time, small differences creep in, files become harder to manage and consistency across environments suffers.&lt;/p&gt;

&lt;p&gt;Modules solve this problem by centralizing the logic. We define the VM pattern once and deploy it anywhere by simply passing different input values. This keeps the configuration consistent, reduces duplication and makes it much easier to maintain and scale the infrastructure as it grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Understanding Module Inputs and Outputs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we begin creating our first module, it helps to understand how information moves between the root module and a child module. A child module cannot directly read variables or resources from the root module. Instead, Terraform requires us to explicitly pass values into the module and explicitly return values from it. This makes modules predictable, reusable and easier to maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Module Inputs&lt;/strong&gt;&lt;br&gt;
Inputs are the values a module needs in order to create its resources. These inputs are defined as variables inside the module, and the root module supplies the actual values when calling the module. In our case, the VM module will need things like the resource group name, resource group location, subnet ID, VM size, admin credentials, NIC settings and allowed ports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Module Outputs&lt;/strong&gt;&lt;br&gt;
Outputs are values that the module exposes back to the root module. This allows the root to access information created inside the module without reaching into its internal resources. For example, the module can return the VM’s ID, NIC name or private IP address so they can be used elsewhere in the configuration or displayed after deployment.&lt;/p&gt;

&lt;p&gt;With this idea of inputs and outputs in mind, we can now begin refactoring our VM, NIC and NSG resources into a dedicated Terraform module.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Creating Our First Terraform Module&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: This is more of "refactoring" our previous Terraform code into a module rather than creating a module from scratch. This is very useful in real-world scenarios where someone created a set of resources that will be re-used repeatedly and you want to turn that Terraform code into a reusable module.&lt;/p&gt;

&lt;p&gt;In this step, we are going to move the VM, NIC and NSG resources into a modules/vm folder and wire them up so they still use the same variables as before.&lt;/p&gt;

&lt;p&gt;The resource group, virtual network and subnet will stay in the root configuration. Our existing variables.tf, outputs.tf and terraform.tfvars will also remain in the root. We will keep using those files to provide values and simply pass them into the new module.&lt;/p&gt;

&lt;p&gt;First, create the folder for the VM module, and the new .tf files like so :&lt;/p&gt;

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

&lt;p&gt;The main.tf file we create will hold the resource blocks for the module, which will be the Virtual Machine, Virtual Machine NIC, NSG, and the NSG association resources. Copy and paste those resources from virtual-machine.tf and nsg.tf files to the main.tf, like so :&lt;/p&gt;

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

&lt;p&gt;After copying the code, you can remove the virtual-machine.tf and nsg.tf files.&lt;/p&gt;

&lt;p&gt;There is one important change we need to make at this point. In the previous weeks, the VM, NIC and NSG resources in virtual-machine.tf and nsg.tf were reading location and resource_group_name directly from the resource group in the root, because all resources were at the root module level. An example of this is referring to resource group name and location attributes like this:&lt;/p&gt;

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

&lt;p&gt;Now that we are inside a child module, we can no longer access root resources this way. Instead, the module should receive the resource group name and location as input variables.&lt;/p&gt;

&lt;p&gt;So, while you are in modules/vm/main.tf, replace any usage of azurerm_resource_group.prod.name with var.rg_name, and any usage of azurerm_resource_group.prod.location with var.rg_location, and usage of azurerm_subnet_prod.id with var.subnet_id, like so:&lt;/p&gt;

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

&lt;p&gt;Next, create modules/vm/variables.tf and define the variables that the module expects. These should match the names you are already using in main.tf for the VM and NSG, like so :&lt;/p&gt;

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

&lt;p&gt;This should address the errors the Visual Studio Code were showing in this manner:&lt;/p&gt;

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

&lt;p&gt;Finally, create modules/vm/outputs.tf. Here we define the values that we want to expose back to the root configuration. These are the same kinds of values you exposed in Week 5, but now they come from the module instead of directly from the VM resource.&lt;/p&gt;

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

&lt;p&gt;As you can see, we only took the outputs that are specific to resources under our main.tf file, and left out the resource group output that still is in the root and not in the module.&lt;/p&gt;

&lt;p&gt;In order to refer to the outputs in our VM module, we must now use module.module_name.output_name syntax at the beginning of our outputs we have defined in our root outputs.tf file, like so:&lt;/p&gt;

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

&lt;p&gt;Notice how the resource group output is still the same, as that is not a resource we included in the VM module.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Using the Module in Our Root Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now that we have created our VM child module and moved all VM-related resources into it, the next step is to connect this module to our root configuration. This is where we pass in the variables from our existing variables.tf and continue using terraform.tfvars just like in previous weeks.&lt;/p&gt;

&lt;p&gt;Create a new main.tf under the root directory (Azure), like so:&lt;/p&gt;

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

&lt;p&gt;Here, we will add a module block to actually call the child module we created under modules/vm and create the VM resource. The syntax to create a module looks like this:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt; is where the module is relative to the root directory.&lt;br&gt;
The rest of the variables are the &lt;strong&gt;Inputs&lt;/strong&gt; that the Module expects to be able to create the module. So what happens when we are calling the VM module is we are passing the variable values in terraform.tfvars file as inputs when the VM is being created using the child module.&lt;/p&gt;

&lt;p&gt;Notice how azurerm_subnet.prod.id is not a variable. It’s an attribute that exists only after the virtual network and subnet are created. Because the VM module depends on the subnet being created first, referencing the subnet ID here creates an implicit dependency. Terraform automatically ensures that the subnet is created before the VM module runs, simply because the module cannot receive this value until the subnet exists.&lt;/p&gt;

&lt;p&gt;With this module block in place, the root configuration is now responsible for providing inputs (via variables and resource outputs), while the child module handles the actual creation of the VM, NIC and NSG. This separation keeps the root clean and makes the VM definition fully reusable for future deployments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Deploying to Azure&lt;/strong&gt;&lt;br&gt;
Okay, that was a lot of editing and moving things around. This week, we'll add some additional commands to make sure everything is in tact.&lt;/p&gt;

&lt;p&gt;Let's start with initializing Terraform by issuing terraform init command:&lt;/p&gt;

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

&lt;p&gt;Since we moved a lot of code around, the formatting in our .tf files may no longer be consistent. Terraform provides a built-in tool for this. Running the terraform fmt command will automatically clean up and standardize the formatting across all your Terraform files so everything is properly aligned and easier to read. Since we have a hierarchical folder structure, in order to apply terraform fmt command to all files in all directories, we will append -recursive flag at the end of terraform fmt command.&lt;/p&gt;

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

&lt;p&gt;Great, looks like i had multiple files that required formatting which are listed above.&lt;/p&gt;

&lt;p&gt;Before applying any changes, it is a good idea to run the terraform validate command. This command checks whether your Terraform files are written correctly and whether the configuration is structurally sound. It does not contact Azure or create any resources. It simply reads your files and confirms that the syntax, references and module structure are valid. Running this helps catch issues early before running a plan or apply:&lt;/p&gt;

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

&lt;p&gt;Now we are going to run terraform plan as usual:&lt;/p&gt;

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

&lt;p&gt;Great! Looks like it's still creating 7 resources (VM, VM NIC, NSG, NSG Association, Virtual Network, Subnet , Resource Group), and outputs are still the same as well. Looks like our module configuration is working.&lt;/p&gt;

&lt;p&gt;Next, we run terraform apply -auto-approve command:&lt;/p&gt;

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

&lt;p&gt;Perfect, all resources are created successfully, and at the end of our apply, we got our outputs printed out.&lt;/p&gt;

&lt;p&gt;Checking the Azure Portal, I can also see the VM created successfully with the values expected:&lt;/p&gt;

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

&lt;p&gt;Finally, don't forget to issue terraform destroy -auto-approve command to prevent incurred costs:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;7. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By introducing our first Terraform module, we transformed our configuration from a simple, single-VM setup into a more modular and reusable design. Modules are how Terraform scales—whether you're deploying five VMs or fifty, the pattern stays the same. This week we learned how to move existing resources into a module, pass data into it from the root and expose outputs back out so other parts of the configuration can use them.&lt;/p&gt;

&lt;p&gt;From here, you’ll start to see why teams rely on modules in every real Terraform project. In the upcoming posts, we’ll expand this module, deploy multiple instances and continue turning our growing configuration into something that mirrors real infrastructure practices.&lt;/p&gt;

&lt;p&gt;Thanks for reading and stay tuned for more!&lt;/p&gt;

</description>
      <category>azure</category>
      <category>terraform</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Terraform Basics – Week 5: Exposing Infrastructure Data with Outputs</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Wed, 03 Dec 2025 18:17:04 +0000</pubDate>
      <link>https://dev.to/ozanguner/terraform-basics-week-5-exposing-infrastructure-data-with-outputs-5h6n</link>
      <guid>https://dev.to/ozanguner/terraform-basics-week-5-exposing-infrastructure-data-with-outputs-5h6n</guid>
      <description>&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Recap of Week 4&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. What Are Output Values in Terraform ?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Why Are They Useful and Where Do I Use Them ?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Adding Outputs to Our Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Common Output Pitfalls and Tips&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Deploy to Azure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Recap of Week 4&lt;/strong&gt;&lt;br&gt;
Last week, we focused on improving the security of our Azure VM by introducing Network Security Groups (NSGs) into our Terraform configuration.&lt;/p&gt;

&lt;p&gt;We created inbound security rules to control access to the VM and attached the NSG to the network interface to enforce those rules. We also used dynamic blocks to keep our configuration clean and avoid repeating ourselves when defining multiple security rules.&lt;/p&gt;

&lt;p&gt;If you missed Week 4, you can check it out the full post &lt;a href="https://dev.to/ozanguner/terraform-basics-week-4-securing-your-azure-vms-with-nsgs-and-dynamic-blocks-3mh0"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This week, we’ll shift our focus to output values and learn how to expose useful information from our Terraform deployments.&lt;/p&gt;

&lt;p&gt;For this week's .tf files, you can access them &lt;a href="https://github.com/OzangunerGH/Terraform-Basics-Series/tree/main" rel="noopener noreferrer"&gt;here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. What Are Output Values in Terraform ?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Output values allow Terraform to display information about your infrastructure after it has been created.&lt;/p&gt;

&lt;p&gt;If you have been paying attention to the CLI output of some of our terraform plan and apply commands from previous weeks, we kept seeing some of the fields saying "known after apply". Here is an example :&lt;/p&gt;

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

&lt;p&gt;If you look at the screenshot, you will see information such as public ip address, dns server, and many more are saying "known after apply". &lt;/p&gt;

&lt;p&gt;And after the apply is complete, Terraform does not necessarily tell you "Here's the public IP information". &lt;/p&gt;

&lt;p&gt;That’s where output values come in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Why Are They Useful and Where Do I Use Them ?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few reasons. Imagine you just deployed a web server using Terraform and you want to SSH or RDP into it to configure it. You will need the public IP address to connect. Without outputs, you would have to log into the Azure Portal and search for it manually.&lt;/p&gt;

&lt;p&gt;With output values, Terraform prints that information directly in your terminal after the deployment finishes. This means you can connect immediately without leaving your terminal.&lt;/p&gt;

&lt;p&gt;Output values also become extremely important once you start working with Terraform modules.&lt;/p&gt;

&lt;p&gt;They allow you to pass values from one Terraform module to another ( more on this later when we introduce Terraform Modules in upcoming weeks) such as resource IDs, IP addresses, resource names, or endpoints. &lt;/p&gt;

&lt;p&gt;If you have a software engineering background, you can think of Terraform outputs the same way you think about return values from a function or properties exposed by a class object.&lt;/p&gt;

&lt;p&gt;Outputs are also useful in automation. A pipeline can deploy infrastructure, read output values, and then use them to configure software or run scripts automatically.&lt;/p&gt;

&lt;p&gt;They make your infrastructure usable, not just deployable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Adding Outputs to Our Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now let’s move into the hands on part and actually add output values to our infrastructure.&lt;/p&gt;

&lt;p&gt;Just like we created a dedicated variables.tf file for our variables, we will do the same for outputs for the same reason. Having all outputs in a single place makes things easier to manage and gives you a clear view of what information your infrastructure is exposing.&lt;/p&gt;

&lt;p&gt;Go ahead and create a file called outputs.tf in your project:&lt;/p&gt;

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

&lt;p&gt;For our current infrastructure, we will output the following values :&lt;/p&gt;

&lt;p&gt;The syntax for an output block looks like this :&lt;/p&gt;

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

&lt;p&gt;The name after the output keyword is important. You want to give each output a name that clearly explains what value is being returned and which resource it belongs to.&lt;/p&gt;

&lt;p&gt;The value section is where you reference the attribute you want from a resource or variable. In this example, the output refers to the name of the resource group we created in previous weeks.&lt;/p&gt;

&lt;p&gt;For our setup, we will create outputs for the following values:&lt;/p&gt;

&lt;p&gt;Private IP address of the virtual machine&lt;br&gt;
Resource group name&lt;br&gt;
Network security group name&lt;br&gt;
The allowed ports defined for the network security group&lt;/p&gt;

&lt;p&gt;Once you add all output blocks, your outputs.tf file should look like this:&lt;/p&gt;

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

&lt;p&gt;Notice how the allowed ports output points directly to the list variable we used in our dynamic block. Since that variable already contains the allowed ports, we can simply expose it as an output.&lt;/p&gt;

&lt;p&gt;This also shows that you are not limited to outputting resource attributes only. You can output variable values as well, which is useful when you want to verify exactly what Terraform is using during a deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Common Output Pitfalls and Tips&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While Outputs are useful, if they are not used correctly they can cause security concerns or deployment issues. Here's a list of common output pitfalls and tips on how to best use Outputs :&lt;/p&gt;

&lt;p&gt;Always make sure you reference the correct resource name and attribute. A small typo can result in empty or incorrect output values.&lt;/p&gt;

&lt;p&gt;Do not reuse output names. Each output must have a unique name or Terraform will fail validation.&lt;/p&gt;

&lt;p&gt;Avoid outputting sensitive information such as passwords or secrets. Outputs are shown in plain text and stored in the state file.&lt;/p&gt;

&lt;p&gt;If an output is showing as null, the attribute may not exist or the value is not available at apply time.&lt;/p&gt;

&lt;p&gt;Use descriptive names for outputs. The output name should clearly explain what value is being returned.&lt;/p&gt;

&lt;p&gt;Only expose outputs that you actually use. If you are not consuming the value, there is no need to output it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Deploy to Azure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As always, we begin by navigating to our Terraform Directory and issuing terraform init command.&lt;/p&gt;

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

&lt;p&gt;Then we issue terraform plan command. Let's have a look at the relevant parts of the plan output for this week :&lt;/p&gt;

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

&lt;p&gt;As you can see in the picture, now we have an Output section that shows us the name of the output (the name we specified while defining the output block), and the values next to it. You can see all of the variables with the exception of private ip is available and reflects the correct values.&lt;/p&gt;

&lt;p&gt;Then we use terraform apply -auto-approve command to deploy our infrastructure: &lt;/p&gt;

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

&lt;p&gt;You can see that the output values are printed following the completion of the apply command, and the private ip information that was not available on the plan output is also printed.&lt;/p&gt;

&lt;p&gt;Finally, don't forget to issue terraform destroy -auto-approve command to avoid infrastructure costs:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;7. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this week’s post, we focused on output values and how they help you expose useful information from your Terraform deployments.&lt;/p&gt;

&lt;p&gt;We looked at how Terraform shows values as "known after apply", how output blocks work, and how to structure outputs using an outputs.tf file. We also added real outputs to our Azure setup and saw how Terraform prints them after a successful deployment.&lt;/p&gt;

&lt;p&gt;At this point, you are no longer just creating infrastructure. You are also making it easier to use, debug, and integrate.&lt;/p&gt;

&lt;p&gt;Next week, we will continue building on this by introducing Terraform modules and start organizing our configuration in a more reusable way.&lt;br&gt;
Hope to see you next week!&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>terraform</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Terraform Basics – Week 4: Securing Your Azure VM's with NSGs and Dynamic Blocks</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Mon, 17 Nov 2025 03:26:30 +0000</pubDate>
      <link>https://dev.to/ozanguner/terraform-basics-week-4-securing-your-azure-vms-with-nsgs-and-dynamic-blocks-3mh0</link>
      <guid>https://dev.to/ozanguner/terraform-basics-week-4-securing-your-azure-vms-with-nsgs-and-dynamic-blocks-3mh0</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Table of Contents&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Recap of Week 3&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. This Week's Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. What Is a Network Security Group (NSG)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Real world challenge with Network Security Groups in Terraform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Using Dynamic Blocks to solve for the challenge&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Deploying to Azure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Recap of Week 3&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Week 3, we focused on improving the structure of our Terraform configuration by introducing variable files. We moved our input values into a dedicated tfvars file and saw how Terraform automatically loads those values during plan and apply. This helped us clean up the main configuration, reduce repetition, and make the deployment easier to adjust without touching the core logic.&lt;/p&gt;

&lt;p&gt;We also talked about why this approach matters in real-world projects, especially when working with multiple environments or collaborating with others. With a cleaner and more flexible setup in place, we can now start enhancing our deployment. You can find the full content for Week 3 &lt;a href="https://dev.to/ozanguner/terraform-basics-week-3-managing-variables-with-tfvars-files-j8i"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This week, we’ll take the next step by adding a Network Security Group and introducing dynamic blocks to define security rules in a more scalable way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. This Week's architecture&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;You can find this week's terraform files &lt;a href="https://github.com/OzangunerGH/Terraform-Basics-Series/tree/main" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. What Is a Network Security Group (NSG)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Network Security Group (aka NSG) is the resource in Azure that lets you control which types of traffic can reach your VM. Instead of leaving your VM open to the internet, an NSG allows you to define rules that specify what should be allowed in or out. For example, you might allow SSH or RDP from your own IP while blocking everything else. If you have worked with Access Control Lists or Firewall Security Rules / Policies before in a network, it's the Azure equivalent of those concepts.&lt;/p&gt;

&lt;p&gt;You can attach an NSG to a subnet or directly to the VM’s network interface (more on that later). Either way, it gives you a straightforward way to lock down access without changing anything inside the VM itself. In this week’s example, we’ll use an NSG to protect the VM we created earlier and make sure only the traffic we expect can reach it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Real world challenge with Network Security Groups in Terraform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Defining an NSG with one or two rules is straightforward. You add a few security_rule blocks to your Terraform file, run apply, and everything works. The challenge starts when you need to open multiple ports, support different source networks, or define separate rules for SSH, RDP, HTTP, HTTPS, and more.&lt;/p&gt;

&lt;p&gt;At that point, your configuration quickly turns into a long list of almost identical blocks. The only parts that change are the name, the port, and sometimes the source address or priority. It still works, but the file becomes harder to read and maintain. Adding a new rule means copying an existing block and editing several fields. Updating a port or changing the allowed network means searching through the file and hoping nothing gets missed.&lt;/p&gt;

&lt;p&gt;This becomes even more problematic when you support multiple environments or need to adjust rules frequently. The repetition adds noise, increases the chance of mistakes, and makes the code harder to follow. To illustrate this, let's start with a simple example.&lt;/p&gt;

&lt;p&gt;Below is a standard NSG with a single inbound rule:&lt;/p&gt;

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

&lt;p&gt;You can also refer to Terraform documentation &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_group" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The structure is simple: define a name, reference the resource group, and include your security_rule blocks underneath.&lt;/p&gt;

&lt;p&gt;But the moment you start adding more rules, the file grows quickly. Here is a version with only three rules:&lt;/p&gt;

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

&lt;p&gt;Even with just three, it’s already harder to scan and understand. Now imagine maintaining this when there are dozens or even hundreds of rules. This is a real challenge in production environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Using Dynamic Blocks to solve for the challenge&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where dynamic blocks come in. Dynamic blocks allow you to generate repeated parts of a resource based on a list or map. In most NSGs, only a few elements actually change per rule: the name, the port, and the allowed network. Everything else stays the same. Instead of writing multiple security_rule blocks by hand, you can loop through the “dynamic” parts and let Terraform create the rules for you.&lt;/p&gt;

&lt;p&gt;To do that, we start by defining a variable that holds the ports we want to allow. This goes in variables.tf:&lt;/p&gt;

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

&lt;p&gt;Then we set our values in terraform.tfvars:&lt;/p&gt;

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

&lt;p&gt;Now we’re ready to create nsg.tf and define our NSG resource. The resource itself is straightforward:&lt;/p&gt;

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

&lt;p&gt;Below that, we add the dynamic block. The syntax always follows the same pattern: dynamic "block_name".&lt;/p&gt;

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

&lt;p&gt;Let’s break down the main parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;for_each:&lt;/strong&gt; This tells Terraform to loop through every element in the list we created earlier. In this example, for_each = var.nsg_allowed_ports means we will generate one security rule for each port.&lt;/p&gt;

&lt;p&gt;Inside the loop, we refer to the current element using security_rule.value&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;name:&lt;/strong&gt; Each security rule needs a unique name. To generate one, we combine a prefix (allow-port-) with the current port number following the syntax shown on the screenshot. This produces values like allow-port-22, allow-port-443, and so on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;priority:&lt;/strong&gt; Each rule must have a unique priority. Here, we calculate the priority based on the index of the port in the list.As you may already know, the first element in the list has an index of 0 and the last element in the list has an index of n-1, n being the number of elements in the list. The first element gets a priority of 100(100+ 100*0), the second gets 200(100+ 100*1), the third gets 300 (100+ 100*2), and so on. This keeps the priorities predictable and avoids collisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;destination_port_range:&lt;/strong&gt;This is simply the port we’re allowing, so we use security_rule.value.&lt;/p&gt;

&lt;p&gt;With one dynamic block, we replaced several repetitive security rule definitions. This makes the code easier to read, easier to update, and easier to scale as your requirements grow. That’s the real benefit of dynamic blocks—they help keep your Terraform configuration clean and maintainable as your infrastructure becomes more complex.&lt;/p&gt;

&lt;p&gt;So now that we have our proper network security group configuration file in place, we still need to associate it with the NIC of our virtual machine.&lt;/p&gt;

&lt;p&gt;Security group associations are defined as a separate resource in Terraform. You don't necessarily have to add it under the nsg.tf file, you can also add it under virtual-machine.tf file, but I'd like to add my associations it under nsg.tf file to see all resources that are associated with that specific NSG under the same file.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;6. Deploying to Azure&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;As always - we start with terraform init :&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;Next we use terraform plan command. I'd like to highlight a few things on the plan output:&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;You can see that our dynamic block is working as we expected, it created 3 rules with the expected values on names, priority and the destination port range attributes.&lt;/p&gt;

&lt;p&gt;Also, our NSG association for the VM is in the plan :&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Now that everything looks good - we go ahead and issue the terraform apply command.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Apply successfully completed and our resources are created. Let's take a look at NSG and how it looks on the Azure side :&lt;/p&gt;

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

&lt;p&gt;Looks exactly like expected, 3 rules created on top of the default rules with expected values.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finally, don't forget to issue terraform destroy command so you don't generate unnecessary costs.&lt;/li&gt;
&lt;/ol&gt;

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

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

&lt;p&gt;&lt;strong&gt;7. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that's a wrap for this week! We focused on strengthening our deployment by introducing a Network Security Group and attaching it to our VM. We also looked at a common challenge that comes up when NSGs start to grow and the number of rules increases. With dynamic block, we reduced repetition, made updates easier, and kept our Terraform files readable as our environment expands.&lt;/p&gt;

&lt;p&gt;Next week, we’ll continue building on this foundation by exploring output values and data types, which will help us expose useful information from our resources and make our configuration more reusable.&lt;/p&gt;

</description>
      <category>security</category>
      <category>terraform</category>
      <category>azure</category>
      <category>devops</category>
    </item>
    <item>
      <title>Terraform Basics Week 3: Managing Variables with tfvars Files</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Mon, 17 Nov 2025 00:44:59 +0000</pubDate>
      <link>https://dev.to/ozanguner/terraform-basics-week-3-managing-variables-with-tfvars-files-j8i</link>
      <guid>https://dev.to/ozanguner/terraform-basics-week-3-managing-variables-with-tfvars-files-j8i</guid>
      <description>&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Recap of Week 2&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. What is a tfvars file, how do I create one and why should i use it ?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Handling Sensitive Values Safely&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Variable Precedence in Terraform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Using .tfvars files instead of environment variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Deploy to Azure – Testing the configuration using tfvars values&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/OzangunerGH/Terraform-Basics-Series/tree/main" rel="noopener noreferrer"&gt;GitHub Link for this week's files&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Recap of Week 2&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Last week we introduced variables in Terraform and saw how they make our configuration more reusable. Instead of hard-coding values directly in resource blocks, we created a variables.tf file and referenced values using &lt;strong&gt;var.variable_name.&lt;/strong&gt;&lt;br&gt;
We also explored how to assign values via defaults and environment variables (for example TF_VAR_location).&lt;/p&gt;

&lt;p&gt;By the end of Week 2 our project structure was cleaner and more scalable, but managing a growing number of values purely through environment variables can become cumbersome. That’s where .tfvars files come in ready to simplify things.&lt;/p&gt;

&lt;p&gt;In case you missed last week's post, you can read the full Week 2 post &lt;a href="https://dev.to/ozanguner/terraform-basics-week-2-variables-and-reusability-2c15"&gt;here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. What is a tfvars file, how do i create one and why should i use it ?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A tfvars file is where you define the values for your variables, separate from your Terraform code. Instead of setting them through environment variables or typing them out every time you run a command, you can store everything in one place and let Terraform pick it up automatically.&lt;/p&gt;

&lt;p&gt;Terraform looks for two files by default:&lt;/p&gt;

&lt;p&gt;terraform.tfvars and any file ending with .auto.tfvars (more on this in upcoming weeks). If either of those exist in your working directory, Terraform automatically loads them when you run plan or apply.&lt;/p&gt;

&lt;p&gt;So let's go back to our Week 2 setup and explain this with our example:&lt;/p&gt;

&lt;p&gt;In order to utilize tfvars file instead of Environment Variables, you would have a variables.tf file, such as the one we had last week:&lt;/p&gt;

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

&lt;p&gt;Then, you would create a file called terraform.tfvars under your project folder, like so :&lt;/p&gt;

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

&lt;p&gt;As shown in the example, this is how you define values for the variables you created in your variables.tf file using the terraform.tfvars file.&lt;br&gt;
Any value you set in terraform.tfvars overrides the variable’s default value if one is defined. The default value is only used when no other value is provided through a tfvars file, the CLI, or environment variables.&lt;/p&gt;

&lt;p&gt;So why use it this way? Why not just set the values directly in the variable’s default attribute or continue using environment variables?&lt;/p&gt;

&lt;p&gt;Because tfvars files offer much better flexibility and management at scale. Imagine having to create environment variables for a hundred different variables. Now imagine doing that across multiple Terraform environments where those values differ each time. Would you manually update or remove environment variables whenever you switch between environments?&lt;/p&gt;

&lt;p&gt;The same challenge applies if you rely on default values. tfvars files solve this by letting you keep multiple versions for different purposes or environments. You can see all variable values for a given project in one place and manage them easily without touching your Terraform code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Handling Sensitive Values Safely&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When using tfvars files, you’ll often define values like usernames, passwords, or API keys. These are considered sensitive values, and Terraform provides a way to protect them from being displayed in your CLI output.&lt;/p&gt;

&lt;p&gt;To do that, mark the variable as sensitive in your variables.tf file:&lt;/p&gt;

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

&lt;p&gt;This prevents Terraform from printing the value in your plan or apply output. You’ll still see that a change is being made, but the actual value will be hidden.&lt;/p&gt;

&lt;p&gt;It’s worth noting that Terraform still stores these values in the state file, but we'll go over how to secure those in upcoming weeks.&lt;/p&gt;

&lt;p&gt;Marking sensitive variables like this helps keep your terminal output, logs, and collaboration history clean while still allowing Terraform to use the values during deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Variable Precedence in Terraform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Terraform runs, it can receive variable values from several places. If a variable is defined in more than one location, Terraform follows a specific order to decide which value to use. Understanding this order helps you predict how your configuration will behave and avoid confusion when values overlap.&lt;/p&gt;

&lt;p&gt;From lowest to highest precedence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Default values set inside the variable block&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Values from terraform.tfvars or any *.auto.tfvars file&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Values passed with the -var or -var-file flag&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment variables that start with TF_VAR_&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a variable is defined in multiple places, Terraform always uses the one that appears later in this list.&lt;/p&gt;

&lt;p&gt;For example, if your terraform.tfvars file contains:&lt;/p&gt;

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

&lt;p&gt;but you have an environment variable defined as:&lt;/p&gt;

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

&lt;p&gt;Terraform will use West Europe, because environment variables override tfvars files.&lt;/p&gt;

&lt;p&gt;Knowing this order is especially helpful when troubleshooting unexpected values or testing changes without modifying your main configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Using terraform.tfvars files instead of environment variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We covered most of what needs to be done for this section in the previous sections actually. To start using terraform.tfvars for assigning values to your variables, create a terraform.tfvars file as described in section 2 and define the values you would like to use for each.&lt;/p&gt;

&lt;p&gt;We also mentioned in variable precedence section that Environment Variables do override tfvars variables. So with that information, if you have Environment Variables defined from the previous weeks, make sure those no longer exist, in order to have your tfvars variables apply on your deployment.&lt;/p&gt;

&lt;p&gt;I had these defined from previous weeks which I now deleted :&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;6. Deploy to Azure – Testing the configuration using tfvars values&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;As always - we start with terraform init :&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;Next we use terraform plan command. I'd like to highlight a few things on the plan output:&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;You can see that because we used sensitive = true attribute for admin_password variable, the value is not shown in the plan output. You can also see resource_group_name value is now rg-prod-002, which is what I had in my terraform.tfvars file and it's overwriting the default attribute that was defined for that variable in variables.tf file. Perfect, just as we expected.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Now that everything looks good - we go ahead and issue the terraform apply command.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;After apply command ran for 1 minutes and 30 seconds, I could see in my Azure Tenant the VM is created with the appropriate values that we defined in terraform.tfvars :&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Finally, don't forget to issue terraform destroy command so you don't generate unnecessary costs.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;7. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This week we replaced environment variables with a terraform.tfvars file and saw how much simpler it is to manage variable values this way. We learned how Terraform automatically reads that file, how its values override defaults, and how to handle sensitive variables safely.&lt;/p&gt;

&lt;p&gt;With this setup, our configuration is cleaner, easier to maintain, and ready to scale. We no longer need to rely on environment variables every time we run a plan or apply.&lt;/p&gt;

&lt;p&gt;Next week, we’ll build on this by improving the security of our deployment. We’ll add a Network Security Group to control access to the VM and use dynamic blocks to make our configuration more flexible. &lt;/p&gt;

&lt;p&gt;I hope this was helpful to you and hope to see you next week for more Terraform fun!&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>azure</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Terraform Basics – Week 2: Variables and Reusability</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Mon, 10 Nov 2025 02:07:30 +0000</pubDate>
      <link>https://dev.to/ozanguner/terraform-basics-week-2-variables-and-reusability-2c15</link>
      <guid>https://dev.to/ozanguner/terraform-basics-week-2-variables-and-reusability-2c15</guid>
      <description>&lt;p&gt;In Week 2 of the Terraform Basics series, we’ll take the configuration we built last week and make it flexible, reusable, and more secure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table Of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Recap: What We Built Last Week&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Improving What We Have - Using Variables for Flexibility &amp;amp; Security&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Updated Terraform Files&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Deploying to Azure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.Recap : What We Built Last Week&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Last week, we built our first Azure Virtual Machine and the prerequisite resources that goes with an Azure VM.&lt;/p&gt;

&lt;p&gt;Here's a visual reminder:&lt;/p&gt;

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

&lt;p&gt;You can check out the full details of Week 1 &lt;a href="https://dev.to/ozanguner/terraform-basics-week-1-deploying-your-first-azure-vm-1f86"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Improving What We Have&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Last week we were able to successfully deploy our first VM to our Azure Tenant. But, the way we deployed it had a lot of bad practices that are not secure, scalable or flexible. This week, we are going to move on to better practices.&lt;/p&gt;

&lt;p&gt; &lt;strong&gt;Using Variables for Flexibility &amp;amp; Security&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Week 1, we hard-coded almost everything. The region of the VM, its size, name, credentials. The reason this is not optimal is because in real-world environments these are going to differ from one deployment to another.&lt;/p&gt;

&lt;p&gt;Let's go back to our architecture for example. We had 4 .tf files and each resource has some variables associated with them. If you wanted to create the exact set of resources in another region with different resource names, you would have to create a copy of all the .tf files, change the name variables of each resource, and then deploy it. You can imagine as the environment you work in gets bigger, this manual effort becomes a real roadblock and prone to errors.&lt;/p&gt;

&lt;p&gt;That is why we use variables.&lt;/p&gt;

&lt;p&gt;In terraform, you can define a variable like so :&lt;/p&gt;

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

&lt;p&gt;Default attribute is optional. In case there are no values provided for the variable, the variable becomes the default value that is specified.&lt;/p&gt;

&lt;p&gt;Sensitive flag redacts the value of the variable from console output, logs and terraform plan outputs, so you don't accidentally leak sensitive information like admin passwords of VMs.&lt;/p&gt;

&lt;p&gt;In order to change our configuration file to have the admin_username and admin_password attributes of the VM to refer to the variables we created, we use the var.variablename format, like so:&lt;/p&gt;

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

&lt;p&gt;Now you may be asking, what are the values of these variables ? We just defined a default value but not the actual value. You are right.&lt;br&gt;
There are two ways to define the values, but for this week's purpose we would only go over one. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment Variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These are the variables you define on your environment where your Terraform Files are hosted, in my case my Windows PC.&lt;/p&gt;

&lt;p&gt;In windows, you can define Environment variables by opening the Start and typing Environment Variables and clicking "Edit the system environment variables" result that comes up. After that, click on "Environment Variables". Select New, and create the environment variable in the format that is shown.&lt;/p&gt;

&lt;p&gt;In order for Terraform to recognize the variable you define here, you must  add TF_VAR_ prefix before the variable name. Variable name also has to match exactly and it's case sensitive.&lt;/p&gt;

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

&lt;p&gt;Once you define as shown in the picture above, you can click "OK" and close the pop-up windows. Close and re-open Visual Studio Code for environment variables to be recognized.&lt;/p&gt;

&lt;p&gt;After completing all the steps described, Terraform will now refer to the variables we created and our username and password will not be hard-coded and less vulnerable than before.&lt;/p&gt;

&lt;p&gt;Here's a challenge for you, using what you have learned above, try to create variables for the name attributes of all the 5 resources we created using the default parameter (No need for environment variables). You can find the solution&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Updated Terraform Files&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Updated File Structure:&lt;/p&gt;

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

&lt;p&gt;Latest version of the files can be found in screenshots below, as well as in this &lt;a href="https://github.com/OzangunerGH/Terraform-Basics-Series/tree/main" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;providers.tf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same as last week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;resource-group.tf&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;variables.tf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here, I used variables for names for all 5 resources that we created, and I also used variables for Virtual Machine's size attribute, resource group's location attribute, and NIC's private IP attribute. Things like IP, VM Size, and Location are often change a lot per resource or project so it always makes sense to use variables for those attributes.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;virtual-machine.tf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Changes are highlighted in red.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;virtual-network.tf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Changes are highlighted in red.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;4. Running Terraform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For Windows, open a command prompt or a PowerShell and navigate to your Terraform project folder you created (in my case, Azure). From your project folder, run the following commands :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;terraform init - Initializes terraform, installs the required providers&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;terraform plan - Shows you what resources are going to be added, deleted, or changed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can see our environment variables in action here by having the username changed to localadmin, and the sensitive tag blocking the password from being displayed in terraform plan output :&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;terraform apply - Terraform provisions each resource exactly as defined.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;terraform destroy - Destroys all the resources defined in your configuration files. After you are done, don't forget to issue this command to avoid a huge bill on your account.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;5. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That wraps up Week 2 of the Terraform Basics series. Each week, we’re getting closer to a real-world Terraform environment. Next week, we’ll cover terraform.tfvars, secure the VM with a Network Security Group, and explore dynamic blocks.&lt;/p&gt;

&lt;p&gt;I hope this was helpful to understand Terraform Basics, and hope to see you again in Week 3!&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>azure</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Terraform Basics – Week 1: Deploying Your First Azure VM</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Sun, 02 Nov 2025 01:22:59 +0000</pubDate>
      <link>https://dev.to/ozanguner/terraform-basics-week-1-deploying-your-first-azure-vm-1f86</link>
      <guid>https://dev.to/ozanguner/terraform-basics-week-1-deploying-your-first-azure-vm-1f86</guid>
      <description>&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Intro&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. What We’re Building This Week&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Creating Our Folder and File Structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Why Multiple .tf Files&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Step by Step Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a) providers.tf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;b) resource-group.tf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;c) virtual-network.tf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;d) virtual-machine.tf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Deploying to Azure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. What We’ll Improve Next Week&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Intro&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Welcome to Week 1 of my new Terraform Basics series.&lt;br&gt;
Each week I will build on what we did before, starting simple and gradually layering in real-world examples and best practices as we go along.&lt;/p&gt;

&lt;p&gt;When you’re used to clicking through Azure Portal, the idea of spinning up infrastructure through code can feel strange. But once you see how clean, repeatable, and scalable it is, there’s no going back. Terraform makes that transition smooth: you describe your setup in plain text, and Terraform builds it exactly the same way every single time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you’d like to follow along, here’s what you’ll need:&lt;/p&gt;

&lt;p&gt;Install Terraform, VS Code and the VS Code Terraform Extension on your machine. &lt;a href="https://www.youtube.com/watch?v=4yvlR4_5ZF0" rel="noopener noreferrer"&gt;Here's&lt;/a&gt; a quick video that demonstrates how to do all 3.&lt;/p&gt;

&lt;p&gt;Give Terraform the necessary access to your Azure Tenant, by following the video &lt;a href="https://www.youtube.com/watch?v=ZoB5cG_zakM" rel="noopener noreferrer"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Once that’s done, you’re ready to go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. What We’re Building This Week&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;We’re starting small: a single Windows virtual machine in Azure, with the basic building blocks it needs to exist: A resource group, virtual network, subnet, and a network interface.&lt;br&gt;
&lt;a href="https://github.com/OzangunerGH/Terraform-Basics-Series" rel="noopener noreferrer"&gt;&lt;br&gt;
Here's the GitHub repository&lt;/a&gt; to access all terraform .tf files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Creating Our Folder and File Structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To get started, we’ll create our Terraform project folder and organize our tf files like this:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;5. Why Multiple .tf Files&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can put everything in a single main.tf file, but organizing your configuration by purpose keeps things cleaner as you expand.&lt;/p&gt;

&lt;p&gt;providers.tf     -       Defines the Azure provider&lt;br&gt;
resource-group.tf   -   Creates a resource group&lt;br&gt;
virtual-network.tf  -   Creates the virtual network and subnet&lt;br&gt;
virtual-machine.tf  -   Creates the NIC and VM&lt;/p&gt;

&lt;p&gt;Think of it like keeping different tools in different drawers. Everything still works together, but finding and modifying parts later becomes much easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Step by Step Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s go through each file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a) providers.tf&lt;/strong&gt;&lt;br&gt;
This tells Terraform which cloud you’re using, in our case Azure.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;b) resource-group.tf&lt;/strong&gt;&lt;br&gt;
A resource group is a logical container for your Azure resources.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;c) virtual-network.tf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The virtual network defines a private address space, and the subnet carves out a smaller range inside it for your VM and any other future resources you may choose to put there.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;d) virtual-machine.tf&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This file defines both the network interface and the VM itself. The NIC connects the VM to the subnet, and the VM block creates the Windows Server 2025 instance.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;7. Deploying to Azure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For Windows, open a command prompt or a PowerShell and navigate to your Terraform project folder you created (in my case, Azure). From your project folder, run the following commands :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;terraform init - Initializes terraform, installs the required providers&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;terraform plan - Shows you what resources are going to be added, 
deleted, or changed.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;terraform apply - Terraform provisions each resource exactly as defined.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;terraform destroy - Destroys all the resources defined in your configuration files. After you are done, don't forget to issue this command to avoid a huge bill on your account.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;8. What We’ll Improve Next Week&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Right now everything is hard-coded. It works, but it’s not flexible or scalable.&lt;/p&gt;

&lt;p&gt;In Week 2 we’ll introduce variables for names, sizes, and credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Wrap-Up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In under a hundred lines of Terraform, we built a complete Azure VM from scratch. No clicking through the portal, no guessing, and fully repeatable every time. When you are deploying resources constantly, using Infrastructure as Code to deploy resources becomes very efficient.&lt;/p&gt;

&lt;p&gt;This is where Infrastructure as Code really shows its value: consistency, transparency, and control.&lt;/p&gt;

&lt;p&gt;I hope this was helpful to understand Terraform Basics, and hope to see you again in Week 2!&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>azure</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Conquer AZ-305 in 4-6 Weeks: Your Roadmap to Azure Solutions Architect Expert</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Tue, 04 Mar 2025 04:56:33 +0000</pubDate>
      <link>https://dev.to/ozanguner/conquer-az-305-in-4-6-weeks-your-roadmap-to-azure-solutions-architect-expert-54fj</link>
      <guid>https://dev.to/ozanguner/conquer-az-305-in-4-6-weeks-your-roadmap-to-azure-solutions-architect-expert-54fj</guid>
      <description>&lt;p&gt;&lt;strong&gt;Because who doesn’t love leveling up their Azure game?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Aim for AZ-305?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Shine as a Cloud Architect:&lt;/strong&gt; Demonstrate real-world expertise in designing top-notch Azure solutions.&lt;br&gt;
&lt;strong&gt;Validate Your Skills:&lt;/strong&gt; Prove you can handle the complexities of Azure from end to end.&lt;br&gt;
&lt;strong&gt;Build on AZ-104:&lt;/strong&gt; You’ve already tackled Azure administration, so now it’s time for the big leagues.&lt;/p&gt;

&lt;p&gt;Note: You must have your AZ-104 (Azure Administrator Associate) under your belt first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AZ-305 Exam at a Glance&lt;/strong&gt;&lt;br&gt;
When you sit down for AZ-305, expect questions focused on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Governance &amp;amp; Management (25-30%)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Azure AD, RBAC, Conditional Access&lt;br&gt;
Azure Policy, Blueprints, Cost Management&lt;br&gt;
Monitoring &amp;amp; Logging&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Architecture (20-25%)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Storage (Blob, Files, Disks)&lt;br&gt;
Azure SQL, Cosmos DB&lt;br&gt;
Backup &amp;amp; Recovery (Azure Backup, ASR)&lt;br&gt;
Infrastructure (35-40%)&lt;/p&gt;

&lt;p&gt;VMs, Containers, AKS&lt;br&gt;
Scaling (Autoscale, VM Scale Sets)&lt;br&gt;
Networking (VNets, VPN, ExpressRoute, Firewall)&lt;br&gt;
High Availability &amp;amp; Disaster Recovery&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application Architecture (20-25%)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Serverless (Azure Functions, Logic Apps)&lt;br&gt;
App Services, API Management&lt;br&gt;
Messaging (Service Bus, Event Grid, Event Hub)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Resources for AZ-305&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/training/paths/microsoft-azure-architect-design-prerequisites/" rel="noopener noreferrer"&gt;Microsoft Learn Path &lt;/a&gt;(AZ-305 Modules + Free Practice Test) ~15 hours&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.measureup.com/microsoft-practice-test-az-305-designing-microsoft-azure-infrastructure-solutions.html?srsltid=AfmBOoo-nEuq1NOOwmRK9JyOEifXCRP2gK3CRMvdi4vnNZ6CsDw6wPZu" rel="noopener noreferrer"&gt;MeasureUp&lt;/a&gt; &amp;amp; &lt;a href="https://portal.tutorialsdojo.com/product/az-305-designing-microsoft-azure-infrastructure-solutions-practice-exams/" rel="noopener noreferrer"&gt;Tutorials Dojo&lt;/a&gt; Practice Tests (Recommended both, ~20 hours each)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/shows/exam-readiness-zone/preparing-for-az-305-design-identity-governance-and-monitoring-solutions-1-of-4" rel="noopener noreferrer"&gt;Exam Readiness Videos&lt;/a&gt; ~2 hours&lt;/li&gt;
&lt;li&gt;
&lt;a href="//4.%20%20%20Watch%20the%20AZ%20305%20Study%20Cram"&gt;John Savill’s AZ-305 Study Cram&lt;/a&gt; ~5 hours&lt;/li&gt;
&lt;li&gt;Finally, the Big Exam&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The 4-6 Week Plan&lt;/strong&gt;&lt;br&gt;
Before you dive into this plan, confirm you’ve already got AZ-104.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weeks 1-4: Get the Fundamentals Down First&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1: Governance &amp;amp; Basics&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start the Microsoft Learn Path for AZ-305 (~4-5 hours).&lt;br&gt;
Play around with Azure Policy, RBAC, and Blueprints in a test environment.&lt;br&gt;
Make sure you fully grasp cost management and monitoring best practices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2: Infrastructure &amp;amp; Security&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keep going with the Microsoft Learn Path (~4-5 more hours).&lt;br&gt;
Spin up and secure VMs, containers, AKS—add some load balancers or autoscaling to see it all in action.&lt;br&gt;
Dive into Azure Security Center and Microsoft Defender for Cloud to get a feel for built-in security features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3: Data Architecture &amp;amp; Backup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Continue (or finish, if you’re on a roll) your Microsoft Learn modules (~4-5 hours).&lt;br&gt;
Experiment with Azure SQL, Cosmos DB, and different storage solutions.&lt;br&gt;
Practice implementing Azure Backup and Site Recovery (ASR)—pretend you’re rescuing your environment after a disaster!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 4: Application Architecture &amp;amp; Final Module Wrap-up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Conclude the Microsoft Learn Path (total ~15 hours).&lt;br&gt;
Deploy serverless apps (Functions, Logic Apps) and test out messaging (Service Bus, Event Grid).&lt;br&gt;
Once the modules are done, watch the Exam Readiness Videos (~2 hours) and John Savill’s AZ-305 Study Cram (~5 hours). It’s like a highlight reel for all you’ve learned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weeks 5-6: Testing &amp;amp; Confidence Boost&lt;/strong&gt;&lt;br&gt;
Practice Tests (MeasureUp &amp;amp; Tutorials Dojo)&lt;/p&gt;

&lt;p&gt;Tackle these now that the learning modules are out of the way (~20 hours each if you do both).&lt;br&gt;
Aim for scoring 80-90% consistently before moving on.&lt;br&gt;
Review &amp;amp; Refine&lt;/p&gt;

&lt;p&gt;Go back to any shaky topics in Microsoft Learn or your notes.&lt;br&gt;
Re-run labs or demos if you didn’t feel 100% confident.&lt;br&gt;
Exam Time!&lt;/p&gt;

&lt;p&gt;Schedule at Microsoft Certifications.&lt;br&gt;
Decide between online or Pearson VUE testing center.&lt;br&gt;
Show up rested and ready to crush it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tips for an Engaging Study Experience&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Make it Real:&lt;/strong&gt; Spin up actual Azure resources. You’ll remember everything better when you’ve broken (and fixed) it yourself.&lt;br&gt;
&lt;strong&gt;Buddy Up:&lt;/strong&gt; If you can, study with a friend or colleague. Sharing war stories about tricky configurations is half the fun.&lt;br&gt;
&lt;strong&gt;Don’t Fear Failure:&lt;/strong&gt; Practice tests are your playground for mistakes—learn from them here, not during the real exam.&lt;br&gt;
**Stay Curious: **Azure evolves like crazy. Keep an eye on announcements so you’re not caught off guard by new updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Word&lt;/strong&gt;&lt;br&gt;
Mastering AZ-305 is your next big leap after AZ-104. By following this plan—finishing all the Microsoft Learn modules, then diving into MeasureUp and Tutorials Dojo practice tests—you’ll develop both the theoretical know-how and the hands-on skills that Azure Solutions Architects rely on every day. Go for it, and remember: every cloud question you answer gets you one step closer to expert status!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Architecting a Scalable and Fault-Tolerant WordPress Application</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Wed, 08 Jan 2025 16:12:13 +0000</pubDate>
      <link>https://dev.to/ozanguner/architecting-a-scalable-and-fault-tolerant-wordpress-application-adp</link>
      <guid>https://dev.to/ozanguner/architecting-a-scalable-and-fault-tolerant-wordpress-application-adp</guid>
      <description>&lt;p&gt;When it comes to monolithic designs, it's often difficult to have it resilient, fault tolerant and scalable compared to decoupled architecture. In this example, we have an EC2 Instance where the frontend, application, and database are all hosted. The goal of this project is to demonstrate the challenges with this architecture and re-architect it to a better design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenges with Tightly Coupled Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Scalability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the first iteration of this project, the single EC2 Instance was responsible for all aspects of the application including the application storage tier, database tier, business logic tier.&lt;br&gt;
This design limits the ability to handle increased traffic. Scaling vertically (upgrading instance size) was the only option, which is expensive and has physical limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Fault Tolerance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A single point of failure meant that if the EC2 instance went down, the entire application—including the database and media files—would become unavailable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Resilience&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With both the database and WordPress files on the same instance, data loss was a real risk in case of instance failure. There was no mechanism for data redundancy.&lt;br&gt;
Recovery would require recreating the instance, restoring the database, and re-uploading media files, leading to significant downtime.&lt;br&gt;
These are the major ones with the original architecture. I would have to mention maintenance activities like patching being extremely difficult and risky, static hard-coded configurations like public IP in WordPress, and operational overhead as honorable mentions as well.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How the New Architecture Addresses These Challenges&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Enhanced Scalability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An Auto Scaling Group (ASG) for the EC2 application tier and an Application Load Balancer (ALB) for distributing traffic, allows us to horizontally and dynamically scale out or in, giving us the flexibility to have a mechanism for dynamic workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Improved Fault Tolerance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A managed Database instance like Amazon RDS, which offers high availability with automatic failover allows us to have a more fault-tolerance architecture. In case anything happens to the EC2 Instance(s), your data is safe. RDS is redundant with multiple copies kept in different availability zones, and AWS manages the maintenance activities like patching and backup for you.&lt;br&gt;
In this architecture, we also replace local storage with Amazon EFS for media files. That provides redundancy across multiple AZs, dynamic scalability in case our data grows, and it can be utilized by multiple compute sources like EC2 in case we need to scale out. And even if an EC2 instance fails, the media files remain available. The ASG automatically launches replacement instances, minimizing downtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Increased Resilience&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Separated the database and media files from the EC2 instance and stored them in managed services designed for durability.&lt;br&gt;
Impact: Data is no longer tied to the lifecycle of the EC2 instance. Backups and redundancy features in RDS and EFS ensure data integrity and quick recovery.&lt;/p&gt;

&lt;p&gt;Honorable mentions for benefits would be less overhead for maintenance and scaling activities.&lt;/p&gt;

&lt;p&gt;So, with all of this in mind, let's get to the execution!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Setting Up the Environment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First things first—getting the infrastructure up and running.&lt;br&gt;
CloudFormation Template: Deployed a CF template to provide the foundational environment.&lt;br&gt;
EC2 Instance: Launched an EC2 instance for the application tier.&lt;br&gt;
Parameters in Systems Manager: Configured WordPress-related parameters like database credentials, DB Endpoints, for seamless management.&lt;br&gt;
Initial Setup for EC2 Instance:&lt;br&gt;
Installed prerequisites using Bash scripts to install WordPress and MariaDB.&lt;br&gt;
Deployed WordPress and connected it to a local MariaDB instance using a public IP.&lt;br&gt;
Ran tests (yes, dog pictures were involved 🐾).&lt;/p&gt;

&lt;p&gt;Challenge: The hardcoded public IP caused issues whenever the IP changed. Clearly, this wasn't sustainable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Introducing Launch Templates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why Launch Templates Matter:&lt;/p&gt;

&lt;p&gt;Manually configuring instances is not only error-prone but also doesn't scale well. In order for us to make horizontally scaling seamless, a launch template is extremely valuable. It provides consistency and ensures that every new instance has the same setup.&lt;/p&gt;

&lt;p&gt;Created a launch template to streamline EC2 instance deployment.&lt;br&gt;
Deployed a new instance using the template and repeated the WordPress initial setup steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Migrating the Database to RDS&lt;/strong&gt;&lt;br&gt;
The next step was to decouple the database from the EC2 instance.&lt;br&gt;
Since we want to have a multiple AZ setup, I started off with creating a subnet group where it included 3 subnets from different availability zones. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High Level Database Migration Steps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Backed up the local MariaDB database.&lt;br&gt;
Updated the DB endpoint parameter in Systems Manager to point to the RDS instance.&lt;br&gt;
Restored the backup to the RDS instance.&lt;br&gt;
Configured WordPress to use the RDS endpoint and stopped the local MariaDB service.&lt;br&gt;
Ran tests to confirm the setup was working seamlessly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Moving Media Files to Amazon EFS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Local storage wasn't cutting it. To make the design more resilient, media files were migrated to an Elastic File System (EFS).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Migration Process:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Created the EFS Filesystem in application subnets. &lt;br&gt;
Installed EFS utilities on the EC2 instance.&lt;br&gt;
Moved media files from the EC2 instance to a temporary folder.&lt;br&gt;
Mounted the EFS filesystem to the WordPress wp-content folder.&lt;br&gt;
Transferred media files back to wp-content and rebooted the instance.&lt;br&gt;
Tested to ensure WordPress was serving media files from EFS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Adding Elasticity with a Load Balancer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What’s scalability without elasticity? This step involved introducing a load balancer and autoscaling capabilities.&lt;/p&gt;

&lt;p&gt;Application Load Balancer involved completing the steps below:&lt;/p&gt;

&lt;p&gt;Created an Application Load Balancer (ALB) with a target group.&lt;br&gt;
Updated the launch template to reference the ALB DNS name instead of the EC2 public IP.&lt;br&gt;
Autoscaling Group:&lt;br&gt;
Configured an Autoscaling Group (ASG) to manage EC2 instances dynamically.&lt;br&gt;
Added scale-in and scale-out policies based on CPU utilization.&lt;br&gt;
Observed the ASG spin up new instances and scale down as required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Words&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This project transformed a single-instance WordPress setup into a robust decoupled architecture. By separating the compute, database, and storage layers, the design became scalable, fault-tolerant, and resilient. The addition of a load balancer and autoscaling ensured elastic performance, while the use of managed services like RDS and EFS reduced operational overhead.&lt;/p&gt;

&lt;p&gt;This architecture serves as a blueprint for modernizing legacy applications while maintaining cost efficiency and high availability. For specific instructions on how to complete each step, make sure to check out this GitHub Repository, here :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/acantril/learn-cantrill-io-labs/tree/master/aws-elastic-wordpress-evolution" rel="noopener noreferrer"&gt;https://github.com/acantril/learn-cantrill-io-labs/tree/master/aws-elastic-wordpress-evolution&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’ve got questions or want to discuss this project further, feel free to reach out!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>On-premise &amp; AWS Hybrid Migration Project</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Thu, 26 Dec 2024 07:03:19 +0000</pubDate>
      <link>https://dev.to/ozanguner/on-premise-aws-hybrid-migration-project-3jaf</link>
      <guid>https://dev.to/ozanguner/on-premise-aws-hybrid-migration-project-3jaf</guid>
      <description>&lt;p&gt;&lt;strong&gt;About This Project&lt;/strong&gt;&lt;br&gt;
As a Cloud Engineer, I’m all for going fully cloud. But in the real world, you’ve got those beloved (ahem, ancient) on-premises AD setups with older protocols. They can’t just vanish overnight, so Hybrid Active Directory is sometimes the hero we need. I’ve done similar migrations in Azure with Azure Files, Azure Virtual Desktop, and Entra ID Domain Services—but I wanted to flex my AWS muscles, too. That’s where Adrian Cantrill’s lab came in!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Lab Architecture Overview&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Connectivity&lt;/strong&gt;&lt;br&gt;
In production, you’d either set up a Site-to-Site VPN (cheaper, easier) or an AWS Direct Connect (for those with deeper pockets and a pressing need for low latency). In this lab, a simple VPC Peering simulates the on-prem environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JumpBoxes&lt;/strong&gt;&lt;br&gt;
Each VPC has a publicly accessible RDP JumpBox (gasp!). Please don’t do that in the real world without MFA, IP restrictions, and maybe a goat sacrifice—kidding on the last one. Just secure it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Step-by-Step Project Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Deploy the CloudFormation Stack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a Key Pair (so you can actually log in).&lt;br&gt;
Deploy the template using &lt;a href="https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https://learn-cantrill-labs.s3.amazonaws.com/aws-hybrid-activedirectory/01_HYBRIDDIR.yaml&amp;amp;stackName=HYBRIDDIR" rel="noopener noreferrer"&gt;this link&lt;/a&gt;. You'll need to  provide the Key Pair name and a domain admin password.&lt;br&gt;
Prepare coffee—this can take a while.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Health Checks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RDP into both JumpBoxes (using the key pair to decrypt the passwords).&lt;br&gt;
Make sure each JumpBox can see stuff in its own VPC. If something’s amiss, now’s a great time for a troubleshooting-induced caffeine boost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Create an AWS Managed AD Directory&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to Directory Services &amp;gt; Create Directory &amp;gt; AWS Managed Microsoft AD.&lt;br&gt;
Fill in details, pick the right subnets, click Create, then wait another 30–40 minutes (another coffee break?).&lt;br&gt;
Once done, spawn a second JumpBox in AWS that’s domain-joined to this new AD. Install RSAT to manage AD.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Trust Between the Two Domains&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Domain Trust Matters&lt;/strong&gt;&lt;br&gt;
Setting up a two-way trust between your on-prem AD and AWS Managed AD saves you from juggling multiple sets of credentials. This means you don’t have to create (and manage) brand-new usernames and passwords in AWS; users can keep their existing on-prem credentials. Fewer credentials also means fewer helpdesk tickets for password resets—so your support team might actually survive the quarter without quitting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the Trust Works&lt;/strong&gt;&lt;br&gt;
When you enter on-prem credentials to access AWS resources, the trust relationship validates you. That doesn’t automatically grant you full access, though—you still need the right permissions on your on-prem account to be added to the correct groups in AWS. With a two-way trust, AWS-to-on-prem authentication is also possible, though from an end-user standpoint, most folks will stick to on-prem credentials unless you specifically create AWS-based accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To set it up:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DNS Conditional Forwarders: On-prem DNS → add a conditional forwarder for the AWS AD domain. The conditional forwarder should be pointing to the AWS AD DNS IPs, available in your directory service resource in AWS Console.&lt;/p&gt;

&lt;p&gt;On-Prem AD Trust: In Active Directory Domains and Trusts, create a two-way forest trust with the AWS domain. Keep track of your trust password— you will need it in the next step.&lt;br&gt;
AWS AD Trust: In AWS Managed AD, set up the two-way trust for your on-prem domain. Use the same password for Trust password in the previous step. For conditional forwarders, add the private IP addresses of on-premise domain controllers.&lt;/p&gt;

&lt;p&gt;Test by adding the on-prem admin to an AWS AD group. If you can do that without the system exploding, you’re good.&lt;br&gt;
(Side note: the whole point of trust is so users don’t juggle 2 sets of credentials. Because let’s be honest—nobody likes 100 password resets a day.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: File Share in AWS (Amazon FSx)&lt;/strong&gt;&lt;br&gt;
Create FSx for Windows File Server, linking it to AWS Managed AD and the same subnets. Grab yet another cup of coffee while it provisions (~30 minutes).&lt;br&gt;
From your on-prem JumpBox, map the share via \DNSNAME\share. If it works, hooray!&lt;/p&gt;

&lt;p&gt;Realistically, you’d do a more elegant rollout with DFS Namespaces and replication, so people don’t lose their minds when data shifts from on-prem to AWS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: AWS WorkSpaces&lt;/strong&gt;&lt;br&gt;
Go to WorkSpaces, register your AWS Managed AD.&lt;br&gt;
Deploy a Workspace and pick an on-prem user account (this works because we set up that two-way trust).&lt;br&gt;
Download the WorkSpaces Client and log in with on-prem credentials. If you see your brand-new desktop, success!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Final Words&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At this point, you’ve laid the groundwork for migrating on-prem file services to AWS FSx, enabling on-prem creds in AWS, and replacing local RDP with Amazon WorkSpaces. In a real-world scenario, you’d perform a more careful, phased cutover (especially for file server migrations), install necessary applications that existed on RDP to the new Workspace environment, and migrate user profile data. Still, this demo shows the basic steps—enough to give you a feel for how on-prem-to-AWS migration can look in practice.&lt;/p&gt;

&lt;p&gt;I hope you enjoyed this article and learned something new along the way.&lt;/p&gt;

&lt;p&gt;Feel free to reach out with any questions!&lt;/p&gt;

&lt;p&gt;Happy building! :)&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Migrating On-Premises Web Server and Database to AWS: A Step-By-Step Guide</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Mon, 01 May 2023 02:13:46 +0000</pubDate>
      <link>https://dev.to/ozanguner/migrating-on-premises-web-server-and-database-to-aws-a-step-by-step-guide-3lc8</link>
      <guid>https://dev.to/ozanguner/migrating-on-premises-web-server-and-database-to-aws-a-step-by-step-guide-3lc8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F16ix4vg8obw56jhr2wrj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F16ix4vg8obw56jhr2wrj.png" alt="DMS Project Architecture" width="800" height="388"&gt;&lt;/a&gt;&lt;br&gt;
Migration to cloud services is increasingly becoming the norm as businesses seek to improve scalability, flexibility, and cost-efficiency. In this article, I'll guide you through my experience of migrating an on-premises web server and database to Amazon Web Services (AWS) using the AWS Database Migration Service (DMS).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Establishing the Lab Environment&lt;/strong&gt;&lt;br&gt;
The migration journey began by setting up the lab environment. To streamline this process, I used a CloudFormation template, which allowed me to create the necessary infrastructure on both the simulated on-premises and AWS side with just one click. The output included separate instances for the Web server and Database, VPCs, and related network resources like subnets and route tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating a VPC Peering Connection&lt;/strong&gt;&lt;br&gt;
Next, I created a VPC peering connection between the AWS and on-premises environments, simulating a Direct Connect or VPN connection between the two. I added a peering connection and updated the necessary routes to three Route Tables: On-prem-public router, AWS Private and Public Router. This connectivity is crucial for enabling seamless interaction between the two environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating an RDS Instance and an EC2 Instance&lt;/strong&gt;&lt;br&gt;
The third step involved creating an RDS Instance to move the on-premises database instance to AWS. While it was being provisioned, I concurrently created an AWS EC2 Instance with Apache and MariaDB to migrate the on-premises application server to the AWS EC2 Instance. This parallel task management significantly expedited the process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrating the Application Server&lt;/strong&gt;&lt;br&gt;
I then migrated the application server, which involved copying all WordPress-related content from the on-premises application server to the AWS EC2 Instance. I accomplished this using secure copy (scp) via the CLI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrating the Database&lt;/strong&gt;&lt;br&gt;
With the application server migration complete, I shifted my focus to migrating the on-premises database. I created a replication instance, and source &amp;amp; destination endpoints in DMS, to create a migration task. Once those were tested and confirmed operational, I executed a migration task to move the on-premises database to the AWS RDS Instance that I provisioned earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updating Configurations&lt;/strong&gt;&lt;br&gt;
As the final step of the migration process, I had to modify the AWS Application Server Database configurations as they were still pointing to the on-premises database instance. I updated them with the private IP address and credentials of the AWS RDS. To test the independent operation of the AWS Application server and RDS DB, I stopped the on-premises instances.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrapping Up&lt;/strong&gt;&lt;br&gt;
Once I confirmed that the migration of the on-premises environment to AWS was successful, I deleted all the resources I had created. This step is crucial to prevent unnecessary costs for resources that are no longer in use.&lt;/p&gt;

&lt;p&gt;In conclusion, migrating an on-premises web server and database to AWS using AWS DMS is a systematic but straightforward process. It requires careful planning and execution but ultimately provides a scalable, flexible, and cost-efficient solution. The key takeaway is that while migrating to AWS can seem daunting, it's a manageable task with the right tools and step-by-step implementation.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
      <category>databasemigration</category>
    </item>
    <item>
      <title>My Journey to Implementing an AWS Client VPN Solution: A Step-By-Step Guide</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Sun, 30 Apr 2023 22:40:15 +0000</pubDate>
      <link>https://dev.to/ozanguner/my-journey-to-implementing-an-aws-client-vpn-solution-a-step-by-step-guide-i43</link>
      <guid>https://dev.to/ozanguner/my-journey-to-implementing-an-aws-client-vpn-solution-a-step-by-step-guide-i43</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe9zp385uy84m983kamsf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe9zp385uy84m983kamsf.png" alt="AWS Client VPN Architecture" width="800" height="382"&gt;&lt;/a&gt;&lt;br&gt;
As remote work and distributed teams become the norm, establishing secure connections to cloud resources has become crucial. Amazon Web Services (AWS) offers an excellent solution for this through AWS Client VPN. In this article, I'll outline my step-by-step experience implementing an AWS Client VPN solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting Up the Lab Environment&lt;/strong&gt;&lt;br&gt;
My journey began by setting up the lab environment. To streamline this process, I used a CloudFormation template, which allowed me to create the necessary infrastructure with just one click. This resulted in the creation of EC2 instances, VPCs, and related subnets. The automation and simplification of infrastructure deployment using CloudFormation templates saved me considerable time and ensured consistency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Establishing Simple AD&lt;/strong&gt;&lt;br&gt;
Next, I created a Simple AD (Active Directory). Since I planned to use identity-based authentication for my client VPN, Simple AD served as my identity provider for users. It offered a standalone, managed directory service, integrated with SAML-based applications and AWS services, providing a seamless security experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating the Server Certificate&lt;/strong&gt;&lt;br&gt;
After setting up Simple AD, I focused on creating the server certificate that the client VPN would use. I used my Ubuntu VM as the local machine to accomplish this. Once created, I uploaded the certificate to AWS using the AWS ACM import CLI command. This certificate is crucial for ensuring secure connections between the client and the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating the Client VPN Endpoint&lt;/strong&gt;&lt;br&gt;
The next step was to create the Client VPN Endpoint within AWS. I did this under the VPC service, where I created a client VPN endpoint and associated it with the A4L-VPC. Afterward, I linked the Endpoint to the corresponding private subnet. This endpoint would serve as the point of contact for clients to establish VPN connections with AWS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Installing AWS Client VPN&lt;/strong&gt;&lt;br&gt;
The final installation stage involved downloading the configuration file for the client VPN and installing the AWS Client VPN. I chose AWS Client VPN for Ubuntu and installed it on my Ubuntu VM. However, I ran into an issue: the VPN Client was crashing instantly.&lt;/p&gt;

&lt;p&gt;After some digging, I discovered that Ubuntu 22.04 had libssl3 installed, while AWS VPN Client needed libssl1 as a dependency. I quickly solved this issue by installing libssl1 on my Ubuntu VM. With that done, I created a new profile using the configuration file I had downloaded earlier.&lt;/p&gt;

&lt;p&gt;To ensure access to necessary resources within the A4L VPC (namely, DNS Servers), I set up an authorization rule for the client VPN endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing the Connection&lt;/strong&gt;&lt;br&gt;
With all the steps completed, it was time to test the connection. I attempted to connect to AWS Client VPN, and much to my relief, the connection was successful. To verify the connectivity to AWS resources, I pinged the directory service's IP address, and the ping worked perfectly, indicating a successful VPN setup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimt0hg043t9rdx0h8toi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimt0hg043t9rdx0h8toi.png" alt="Connection Successful!" width="419" height="185"&gt;&lt;/a&gt;&lt;br&gt;
In conclusion, my journey to setting up an AWS Client VPN solution was a successful one. While the setup process involved several steps, the result was a secure, reliable, and scalable VPN solution that can cater to a variety of business needs. It's important to remember that every setup might encounter challenges, like the one I faced with my Ubuntu VM, but with a bit of troubleshooting, they can be quickly resolved.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>vpn</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>HybridDNS Implementation Between AWS and On-Premise Environment</title>
      <dc:creator>Ozan Guner</dc:creator>
      <pubDate>Sat, 29 Apr 2023 22:12:01 +0000</pubDate>
      <link>https://dev.to/ozanguner/hybriddns-implementation-between-aws-and-on-premise-environment-1932</link>
      <guid>https://dev.to/ozanguner/hybriddns-implementation-between-aws-and-on-premise-environment-1932</guid>
      <description>&lt;p&gt;&lt;strong&gt;How to Setup a Hybrid DNS between your On-Prem and AWS Environment&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;If you're looking for a way to set up a Hybrid DNS system that allows smooth DNS queries between your on-premise and AWS environments, you're in the right place.&lt;/p&gt;

&lt;p&gt;I've created a Hybrid DNS setup using AWS resources, which simulates a real-world connection between AWS and an on-premise environment. I've also put together easy-to-follow steps so others who need this solution can benefit from it. This is your go-to guide for a user-friendly Hybrid DNS setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Set up the lab environment:&lt;/strong&gt; A CloudFormation template is used here to create the infrastructure in AWS. CloudFormation is a service offered by AWS that enables users to define and provision the resources needed for applications across all regions and accounts. The template describes what resources are needed and AWS CloudFormation takes care of provisioning and configuring those resources. The mentioned resources here are EC2 instances, and Virtual Private Clouds (VPCs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Create VPC Peering connection:&lt;/strong&gt; VPC Peering is a networking connection between two VPCs that enables you to route traffic between them privately. In this case, it's used to connect the AWS VPC with the on-premises VPC. Once the peering connection is created, you add necessary routes to VPC route tables of each VPC to enable the connectivity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Test the connectivity:&lt;/strong&gt; You would then check the connectivity by pinging private IP addresses of servers from each environment (AWS and On-Premises). Pinging is a diagnostic tool to test connectivity between two nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Create Inbound Endpoints and Configure DNS servers:&lt;/strong&gt; To enable DNS resolving, inbound endpoints are created in AWS VPC. These endpoints act as targets for DNS queries. The on-premises DNS servers are then configured to forward all DNS queries to these inbound endpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Connect to App server and configure DNS server IP addresses:&lt;/strong&gt; The app server needs to know where to query for DNS entries, hence the IP addresses of the DNS servers are added in the app server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Create Outbound Endpoints:&lt;/strong&gt; In the AWS VPC, outbound endpoints are created. These endpoints are designed to forward any queries made to a certain domain (e.g., corp.aws) to the DNS servers in the on-premises VPC.  Forward rule has to be setup in Route53.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Enable DNS querying across environments:&lt;/strong&gt; With the above steps, both on-premises clients and resources on AWS can query for DNS entries on each side, ensuring seamless communication across the hybrid environment.&lt;/p&gt;

&lt;p&gt;Credits and special thanks to  Adrian Cantrill for creating this project, which is used quite often in production environments.&lt;/p&gt;

</description>
      <category>cloudcomputing</category>
      <category>aws</category>
      <category>hybriddns</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
