Skip to main content

Terraform Tutorial for .NET Developers: AWS & Azure IaC

Learn Terraform for .NET developers with this hands-on tutorial. Deploy ASP.NET Core apps to AWS and Azure using infrastructure as code. Start today!

If you're a .NET developer who has ever clicked through the Azure Portal or AWS Console to create resources by hand, you already know the pain: environments drift apart, nobody remembers who changed what, and reproducing production in a staging environment takes days. This Terraform tutorial is written specifically for .NET developers who want to solve that problem with infrastructure as code (IaC) — defining your cloud infrastructure in version-controlled files, just like your C# code.

In this guide, you'll learn what Terraform is, why it beats manual provisioning, and how to deploy a real ASP.NET Core application to both Azure App Service and AWS Elastic Beanstalk using Terraform. We'll also cover best practices, common pitfalls, and how Terraform compares to Bicep and AWS CloudFormation — the questions every .NET team asks before adopting it.

What Is Terraform and Why Should .NET Developers Care?

Terraform, created by HashiCorp, is the most widely used infrastructure as code tool in the industry. You describe your desired infrastructure — app services, databases, storage accounts, load balancers — in declarative configuration files written in HCL (HashiCorp Configuration Language). Terraform then compares your desired state to what actually exists in the cloud and creates, updates, or destroys resources to make them match.

Here's why this matters for .NET teams specifically:

  • One tool for AWS and Azure. Many enterprise .NET shops are multi-cloud. Azure Bicep only works on Azure; CloudFormation only works on AWS. Terraform works on both (plus GCP, Cloudflare, GitHub, and 3,000+ other providers), so your team learns one workflow.
  • Code review for infrastructure. Your infrastructure lives in Git next to your C# solution. Changes go through pull requests. terraform plan shows a diff of exactly what will change before anything is touched — like a compiler warning for your cloud.
  • Reproducible environments. Spin up an identical dev, staging, and production environment from the same code with different variable files. No more "it works in staging" mysteries caused by hand-configured settings.
  • It's declarative, like LINQ. You declare what you want, not how to get there. Terraform figures out the dependency graph and execution order, the same way LINQ figures out query execution.

Installing Terraform and the Core Workflow

Install Terraform on Windows with winget (or use Homebrew on macOS, apt on Linux):

winget install HashiCorp.Terraform
terraform -version

Every Terraform project follows the same four-command workflow. Memorize this — it's 90% of your daily usage:

  • terraform init — downloads providers (like restoring NuGet packages)
  • terraform plan — shows what will change (like a dry run)
  • terraform apply — makes the changes
  • terraform destroy — tears everything down

Terraform Tutorial: Deploy ASP.NET Core to Azure App Service

Let's deploy a real ASP.NET Core 8 app to Azure. Create a folder called infra next to your solution and add a file named main.tf:

# main.tf — Azure App Service for an ASP.NET Core app
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "environment" {
  type    = string
  default = "dev"
}

resource "azurerm_resource_group" "main" {
  name     = "rg-csharpcoder-${var.environment}"
  location = "eastus2"
}

resource "azurerm_service_plan" "main" {
  name                = "asp-csharpcoder-${var.environment}"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  os_type             = "Linux"
  sku_name            = var.environment == "prod" ? "P1v3" : "B1"
}

resource "azurerm_linux_web_app" "main" {
  name                = "app-csharpcoder-${var.environment}"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  service_plan_id     = azurerm_service_plan.main.id

  site_config {
    application_stack {
      dotnet_version = "8.0"
    }
    always_on = var.environment == "prod"
  }

  app_settings = {
    "ASPNETCORE_ENVIRONMENT" = var.environment == "prod" ? "Production" : "Development"
  }
}

output "app_url" {
  value = "https://${azurerm_linux_web_app.main.default_hostname}"
}

Notice the ternary expressions — they work almost exactly like C#'s conditional operator. Production gets a Premium plan with always_on; dev gets a cheap Basic plan. Now deploy it:

az login                        # authenticate with Azure CLI
terraform init                  # download the azurerm provider
terraform plan                  # review: "Plan: 3 to add, 0 to change, 0 to destroy"
terraform apply -auto-approve   # create the resources

In under two minutes you have a resource group, an App Service plan, and a web app — and you can recreate them in any subscription, any region, any time. Publish your app with the .NET CLI:

dotnet publish -c Release -o ./publish
cd publish && zip -r ../app.zip .
az webapp deploy --resource-group rg-csharpcoder-dev `
  --name app-csharpcoder-dev --src-path ../app.zip --type zip

Terraform on AWS: The Same Skills, Different Provider

Here's the payoff of learning Terraform instead of a cloud-specific tool: deploying the same app to AWS uses identical concepts. Only the resource types change. This example provisions Elastic Beanstalk, AWS's closest equivalent to Azure App Service for .NET workloads:

# aws.tf — Elastic Beanstalk for the same ASP.NET Core app
provider "aws" {
  region = "us-east-1"
}

resource "aws_elastic_beanstalk_application" "main" {
  name        = "csharpcoder-app"
  description = "ASP.NET Core 8 application"
}

resource "aws_elastic_beanstalk_environment" "main" {
  name                = "csharpcoder-${var.environment}"
  application         = aws_elastic_beanstalk_application.main.name
  solution_stack_name = "64bit Amazon Linux 2023 v3.5.6 running .NET 8"

  setting {
    namespace = "aws:autoscaling:launchconfiguration"
    name      = "InstanceType"
    value     = var.environment == "prod" ? "t3.medium" : "t3.micro"
  }

  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name      = "ASPNETCORE_ENVIRONMENT"
    value     = var.environment == "prod" ? "Production" : "Development"
  }
}

The mental model — providers, resources, variables, outputs, plan, apply — is exactly the same. That transferability is why Terraform consistently tops developer surveys for infrastructure tooling.

Terraform State: The Concept That Trips Up Every Beginner

Terraform records everything it manages in a state file (terraform.tfstate). This file maps your HCL code to real cloud resource IDs. Two rules will save you enormous pain:

  • Never commit state to Git. It contains secrets in plain text (connection strings, keys). Add *.tfstate* and .terraform/ to .gitignore immediately.
  • Use remote state for teams. Local state breaks the moment two developers run apply. Store state in Azure Blob Storage or AWS S3 with locking:
terraform {
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "stcsharpcodertfstate"
    container_name       = "tfstate"
    key                  = "csharpcoder.prod.tfstate"
    use_azuread_auth     = true
  }
}

Think of remote state like a shared database with row locking, versus local state being an in-memory dictionary — fine solo, catastrophic under concurrency.

Best Practices for .NET Teams Using Terraform

1. Structure your repo like a .NET solution

Keep infrastructure in an infra/ folder in the same repository as your application, with one folder (or variable file) per environment. Use modules the way you use class libraries — reusable, parameterized building blocks:

infra/
├── modules/
│   └── webapp/          # reusable module, like a class library
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── environments/
│   ├── dev.tfvars
│   └── prod.tfvars
└── main.tf

2. Never hardcode secrets

Reference Azure Key Vault or AWS Secrets Manager instead of putting connection strings in .tf files. In App Service, use Key Vault references in app_settings so secrets never touch your repo or your state outputs.

3. Run Terraform in CI/CD, not on laptops

Production changes should flow through GitHub Actions or Azure DevOps: plan on pull request (posted as a PR comment for review), apply on merge. This gives you an audit trail and eliminates "who has the right credentials on their machine" problems.

4. Pin your provider versions

Just as you pin NuGet package versions, pin providers with ~> 4.0 constraints and commit the .terraform.lock.hcl file. Unpinned providers are how a routine Tuesday deploy becomes an outage.

Common Pitfalls to Avoid

  • Editing resources in the portal after Terraform created them. This causes drift — the next apply may silently revert someone's "quick fix." Make the rule absolute: if Terraform manages it, only Terraform changes it. Use terraform plan regularly to detect drift.
  • Ignoring the plan output. The single most important habit is reading the plan before applying. A resource marked -/+ destroy and then create replacement means downtime and possible data loss — for example, changing a database server name forces recreation.
  • Running terraform destroy against the wrong workspace. Separate state files per environment and require manual approval gates for production applies in your pipeline.
  • Treating Terraform as a deployment tool. Terraform provisions infrastructure; it shouldn't push your application binaries. Deploy your ASP.NET Core code with dotnet publish plus your pipeline's deploy step, and let Terraform own the platform underneath.

Terraform vs Bicep vs CloudFormation: Which Should You Choose?

If your organization is 100% Azure forever, Bicep is a fine choice with excellent Azure integration and no state file to manage. If you're 100% AWS, CloudFormation (or the CDK, which lets you write infrastructure in C#) is viable. But if there's any chance you'll touch a second cloud, manage GitHub repos, DNS, or SaaS tooling as code, Terraform's single workflow across thousands of providers wins. Its job-market value is also significantly higher — "Terraform" appears in far more DevOps and platform engineering job postings than Bicep and CloudFormation combined.

Conclusion: Key Takeaways from This Terraform Tutorial

This Terraform tutorial covered everything a .NET developer needs to start practicing infrastructure as code today. The key takeaways:

  • Terraform lets you define AWS and Azure infrastructure declaratively, version it in Git, and review changes like any C# pull request.
  • The core workflow is four commands: init, plan, apply, destroy — and plan is the one you should never skip reading.
  • State is the heart of Terraform: keep it out of Git, store it remotely with locking, and separate it per environment.
  • Structure infrastructure like a .NET solution — modules as class libraries, tfvars per environment — and run everything through CI/CD.
  • Choose Terraform over Bicep or CloudFormation when portability, ecosystem breadth, or career value matters.

Your next step: take the Azure example above, run terraform apply against a free-tier subscription, then terraform destroy when you're done — the full loop costs pennies and teaches more than any article can. Once that clicks, move your real project's infrastructure into code, one resource at a time.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments