Deploying a .NET App on Azure: Setting Up a Linux VM from Scratch: Part 01 - Azure CLI, Resource Groups, ARM Templates, SSH Key Pairs, Ubuntu VM

Deploying a .NET App on Azure: Setting Up a Linux VM from Scratch: Part 01 - Azure CLI, Resource Groups, ARM Templates, SSH Key Pairs, Ubuntu VM

Azure Virtual Machines (VMs) provide scalable and flexible computing resources in Microsoft's cloud, enabling organizations to deploy, manage, and scale applications efficiently without maintaining physical hardware.

Azure VMs offer Infrastructure as a Service (IaaS), allowing users to run software applications or services directly on virtualized hardware. Each VM can be customized based on requirements, including OS selection (Windows or Linux), computing power (CPU, RAM), storage, and network configurations.

Key Features

  • Custom Images: Create and reuse custom VM images for rapid deployment.
  • Managed Disks: Simplified disk management and enhanced performance.
  • Security: Built-in security with Azure Security Center, encryption, and network security groups.
  • Automation: Automate deployments and configurations using Azure Resource Manager (ARM) templates, Azure PowerShell, or Azure CLI.

Common Use Cases

  • Web hosting
  • Development and testing environments
  • Running mission-critical applications
  • Disaster recovery and backup solutions

Azure Free Tier

Azure Free Tier is an ideal option for developers, students, and anyone interested in exploring Microsoft Azure without incurring any cost. Azure's Free Tier provides you with a generous allowance of services and resources that allow you to experiment, learn, and develop applications.

The Azure Free Tier includes:

  • 12 Months of Free Services: Popular services like Azure Virtual Machines (750 hours per month), Azure SQL Database (250 GB), Azure Cosmos DB (400 RU/s throughput), and Azure Blob Storage (5 GB) are available for a full year.
  • Always-Free Services: Certain services, such as Azure Functions (1 million requests per month), Azure DevOps (5 users with unlimited private Git repositories), and Azure App Service (10 web, mobile, or API apps) remain free indefinitely, subject to specific limits.
  • $200 Credit for 30 Days: Initially, you also receive a $200 Azure credit valid for 30 days, which lets you explore premium features and experiment without commitment.

In this tutorial, I am using an Azure Pay-As-You-Go subscription since my free tier credits have expired.

Why I Use Linux Over Windows

Deploying ASP.NET Core applications on Linux VMs offers several advantages over traditional Windows environments. Primarily, Linux VMs tend to be more cost-effective due to lower licensing costs, making them a budget-friendly option for scalable solutions. Additionally, Linux provides enhanced stability and performance, crucial for running high-availability ASP.NET Core applications efficiently.

Linux VMs also integrate seamlessly with popular open-source tools and frameworks, facilitating continuous integration and continuous deployment (CI/CD) processes and simplifying automation tasks. Lastly, using Linux encourages broader compatibility and flexibility, empowering development teams to leverage containerization technologies like Docker and Kubernetes, further streamlining the deployment pipeline and maximizing operational efficiency.

Setting up a Virtual Machine

First, let's log in to Azure from Azure CLI, using the following command:

az login        

This will open a web browser and prompt you to authenticate with Azure. After successful authenticaion you can see a similar output on your terminal.


Now we need to create a Resource Group. An Azure Resource Group is a fundamental component in Azure used to organize and manage cloud resources. It acts as a logical container, allowing you to group related resources for easier management, monitoring, and access control.

To create a Resource Group, use the following command:

az group create --name rg-linux-vm --location eastus        

We can see the following output after the resource group is created.


Also, we can verify the resource groups we created using the following command:

az group list --output table        

Now, let’s move forward and create our virtual machine using an Azure Resource Manager (ARM) template. Although Microsoft is gradually shifting towards Bicep as the recommended method, ARM templates remain powerful and widely used.

In this tutorial, I’ll demonstrate how to deploy a VM using an ARM template. Later, we can explore newer options like Bicep or Terraform in upcoming posts.

Generating a SSH Key Pair

Before creating the ARM template, it's best practice to generate an SSH key pair, as using key-based authentication provides a more secure method for accessing your VMs compared to traditional password authentication.

Since I'm using macOS, I'll demonstrate how to generate an SSH key pair specifically for that environment. However, if you're using Windows, you can easily generate SSH key pairs using tools like PuTTYgen, Windows Subsystem for Linux (WSL), or PowerShell. A quick online search will provide detailed instructions tailored to your Windows setup. Regardless of the operating system, using SSH key pairs significantly improves security when connecting to virtual machines.

Run the following command to create a key pair using the RSA algorithm with a 4096-bit key length:

ssh-keygen -m PEM -t rsa -b 4096 -f ~/.ssh/id_rsa.pem        

Here is a breakdown for the above bash command:

? -m PEM: Ensures the private key is in PEM format.

? -t rsa: Specifies the RSA algorithm.

? -b 4096: Sets the key length to 4096 bits.

? -f ~/.ssh/id_rsa.pem: Defines the file path and name for the private key.

When prompted, you can set a passphrase for added security or press Enter to skip.

This process generates two files:

Ensure the private key (id_rsa.pem) remains confidential and is not shared.

ARM Template for the Virtual Machine

Azure Resource Manager (ARM) templates are JSON-based infrastructure-as-code (IaC) solutions that enable users to define and deploy Azure resources consistently and reliably. ARM templates simplify managing cloud infrastructure by providing a declarative syntax to outline the desired state of resources, configurations, and dependencies.

Key advantages of ARM templates include:

  1. Consistency and Repeatability:?Deploy resources identically across multiple environments.
  2. Automation and Efficiency:?Quickly provision infrastructure without manual intervention, reducing human error.
  3. Version Control:?Easily track changes and maintain infrastructure history in source control systems like Git.
  4. Reusable Components:?Create modular templates and reuse components to save time and promote consistency.
  5. Integrated Validation:?Built-in validation helps catch errors before deployment, minimizing downtime.

Typical scenarios for ARM template usage:

  • Automating the setup of development, staging, and production environments.
  • Ensuring compliance and governance by enforcing infrastructure policies.
  • Implementing disaster recovery environments quickly and reliably.
  • Streamlining large-scale deployments through automation and standardization.

ARM templates are transitioning towards Bicep, Microsoft's new DSL for infrastructure, providing an even simpler and more intuitive authoring experience. However, understanding ARM templates remains valuable due to their widespread use, deep integration, and robust feature set.

Below is the ARM template we’ll be using to provision the virtual machine (VM) environment:

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "metadata": {
    "_generator": {
      "name": "bicep",
      "version": "0.25.53.49325",
      "templateHash": "2386182166504519146"
    }
  },
  "parameters": {
    "vmName": {
      "type": "string",
      "defaultValue": "ubuntu-vm",
      "metadata": {
        "description": "The name of your Virtual Machine."
      }
    },
    "adminUsername": {
      "type": "string",
      "metadata": {
        "description": "Username for the Virtual Machine."
      }
    },
    "authenticationType": {
      "type": "string",
      "defaultValue": "sshPublicKey",
      "allowedValues": [
        "sshPublicKey",
        "password"
      ],
      "metadata": {
        "description": "Type of authentication to use on the Virtual Machine. SSH key is recommended."
      }
    },
    "adminPasswordOrKey": {
      "type": "securestring",
      "metadata": {
        "description": "SSH Key or password for the Virtual Machine. SSH key is recommended."
      }
    },
    "dnsLabelPrefix": {
      "type": "string",
      "defaultValue": "[toLower(format('{0}-{1}', parameters('vmName'), uniqueString(resourceGroup().id)))]",
      "metadata": {
        "description": "Unique DNS Name for the Public IP used to access the Virtual Machine."
      }
    },
    "ubuntuOSVersion": {
      "type": "string",
      "defaultValue": "Ubuntu-2204",
      "allowedValues": [
        "Ubuntu-2004",
        "Ubuntu-2204"
      ],
      "metadata": {
        "description": "The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version."
      }
    },
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]",
      "metadata": {
        "description": "Location for all resources."
      }
    },
    "vmSize": {
      "type": "string",
      "defaultValue": "Standard_B1s",
      "metadata": {
        "description": "The size of the VM"
      }
    },
    "virtualNetworkName": {
      "type": "string",
      "defaultValue": "vNet",
      "metadata": {
        "description": "Name of the VNET"
      }
    },
    "subnetName": {
      "type": "string",
      "defaultValue": "Subnet",
      "metadata": {
        "description": "Name of the subnet in the virtual network"
      }
    },
    "networkSecurityGroupName": {
      "type": "string",
      "defaultValue": "SecGroupNet",
      "metadata": {
        "description": "Name of the Network Security Group"
      }
    },
    "securityType": {
      "type": "string",
      "defaultValue": "TrustedLaunch",
      "allowedValues": [
        "Standard",
        "TrustedLaunch"
      ],
      "metadata": {
        "description": "Security Type of the Virtual Machine."
      }
    }
  },
  "variables": {
    "imageReference": {
      "Ubuntu-2004": {
        "publisher": "Canonical",
        "offer": "0001-com-ubuntu-server-focal",
        "sku": "20_04-lts-gen2",
        "version": "latest"
      },
      "Ubuntu-2204": {
        "publisher": "Canonical",
        "offer": "0001-com-ubuntu-server-jammy",
        "sku": "22_04-lts-gen2",
        "version": "latest"
      }
    },
    "publicIPAddressName": "[format('{0}PublicIP', parameters('vmName'))]",
    "networkInterfaceName": "[format('{0}NetInt', parameters('vmName'))]",
    "osDiskType": "Standard_LRS",
    "subnetAddressPrefix": "10.1.0.0/24",
    "addressPrefix": "10.1.0.0/16",
    "linuxConfiguration": {
      "disablePasswordAuthentication": true,
      "ssh": {
        "publicKeys": [
          {
            "path": "[format('/home/{0}/.ssh/authorized_keys', parameters('adminUsername'))]",
            "keyData": "[parameters('adminPasswordOrKey')]"
          }
        ]
      }
    },
    "securityProfileJson": {
      "uefiSettings": {
        "secureBootEnabled": true,
        "vTpmEnabled": true
      },
      "securityType": "[parameters('securityType')]"
    },
    "extensionName": "GuestAttestation",
    "extensionPublisher": "Microsoft.Azure.Security.LinuxAttestation",
    "extensionVersion": "1.0",
    "maaTenantName": "GuestAttestation",
    "maaEndpoint": "[substring('emptystring', 0, 0)]"
  },
  "resources": [
    {
      "type": "Microsoft.Network/networkInterfaces",
      "apiVersion": "2023-09-01",
      "name": "[variables('networkInterfaceName')]",
      "location": "[parameters('location')]",
      "properties": {
        "ipConfigurations": [
          {
            "name": "ipconfig1",
            "properties": {
              "subnet": {
                "id": "[reference(resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName')), '2023-09-01').subnets[0].id]"
              },
              "privateIPAllocationMethod": "Dynamic",
              "publicIPAddress": {
                "id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
              }
            }
          }
        ],
        "networkSecurityGroup": {
          "id": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]"
        }
      },
      "dependsOn": [
        "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]",
        "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]",
        "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]"
      ]
    },
    {
      "type": "Microsoft.Network/networkSecurityGroups",
      "apiVersion": "2023-09-01",
      "name": "[parameters('networkSecurityGroupName')]",
      "location": "[parameters('location')]",
      "properties": {
        "securityRules": [
          {
            "name": "SSH",
            "properties": {
              "priority": 1000,
              "protocol": "Tcp",
              "access": "Allow",
              "direction": "Inbound",
              "sourceAddressPrefix": "*",
              "sourcePortRange": "*",
              "destinationAddressPrefix": "*",
              "destinationPortRange": "22"
            }
          }
        ]
      }
    },
    {
      "type": "Microsoft.Network/virtualNetworks",
      "apiVersion": "2023-09-01",
      "name": "[parameters('virtualNetworkName')]",
      "location": "[parameters('location')]",
      "properties": {
        "addressSpace": {
          "addressPrefixes": [
            "[variables('addressPrefix')]"
          ]
        },
        "subnets": [
          {
            "name": "[parameters('subnetName')]",
            "properties": {
              "addressPrefix": "[variables('subnetAddressPrefix')]",
              "privateEndpointNetworkPolicies": "Enabled",
              "privateLinkServiceNetworkPolicies": "Enabled"
            }
          }
        ]
      }
    },
    {
      "type": "Microsoft.Network/publicIPAddresses",
      "apiVersion": "2023-09-01",
      "name": "[variables('publicIPAddressName')]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "Basic"
      },
      "properties": {
        "publicIPAllocationMethod": "Dynamic",
        "publicIPAddressVersion": "IPv4",
        "dnsSettings": {
          "domainNameLabel": "[parameters('dnsLabelPrefix')]"
        },
        "idleTimeoutInMinutes": 4
      }
    },
    {
      "type": "Microsoft.Compute/virtualMachines",
      "apiVersion": "2023-09-01",
      "name": "[parameters('vmName')]",
      "location": "[parameters('location')]",
      "properties": {
        "hardwareProfile": {
          "vmSize": "[parameters('vmSize')]"
        },
        "storageProfile": {
          "osDisk": {
            "createOption": "FromImage",
            "managedDisk": {
              "storageAccountType": "[variables('osDiskType')]"
            }
          },
          "imageReference": "[variables('imageReference')[parameters('ubuntuOSVersion')]]"
        },
        "networkProfile": {
          "networkInterfaces": [
            {
              "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]"
            }
          ]
        },
        "osProfile": {
          "computerName": "[parameters('vmName')]",
          "adminUsername": "[parameters('adminUsername')]",
          "adminPassword": "[parameters('adminPasswordOrKey')]",
          "linuxConfiguration": "[if(equals(parameters('authenticationType'), 'password'), null(), variables('linuxConfiguration'))]"
        },
        "securityProfile": "[if(equals(parameters('securityType'), 'TrustedLaunch'), variables('securityProfileJson'), null())]"
      },
      "dependsOn": [
        "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]"
      ]
    },
    {
      "condition": "[and(and(equals(parameters('securityType'), 'TrustedLaunch'), variables('securityProfileJson').uefiSettings.secureBootEnabled), variables('securityProfileJson').uefiSettings.vTpmEnabled)]",
      "type": "Microsoft.Compute/virtualMachines/extensions",
      "apiVersion": "2023-09-01",
      "name": "[format('{0}/{1}', parameters('vmName'), variables('extensionName'))]",
      "location": "[parameters('location')]",
      "properties": {
        "publisher": "[variables('extensionPublisher')]",
        "type": "[variables('extensionName')]",
        "typeHandlerVersion": "[variables('extensionVersion')]",
        "autoUpgradeMinorVersion": true,
        "enableAutomaticUpgrade": true,
        "settings": {
          "AttestationConfig": {
            "MaaSettings": {
              "maaEndpoint": "[variables('maaEndpoint')]",
              "maaTenantName": "[variables('maaTenantName')]"
            }
          }
        }
      },
      "dependsOn": [
        "[resourceId('Microsoft.Compute/virtualMachines', parameters('vmName'))]"
      ]
    }
  ],
  "outputs": {
    "adminUsername": {
      "type": "string",
      "value": "[parameters('adminUsername')]"
    },
    "hostname": {
      "type": "string",
      "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName')), '2023-09-01').dnsSettings.fqdn]"
    },
    "sshCommand": {
      "type": "string",
      "value": "[format('ssh {0}@{1}', parameters('adminUsername'), reference(resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName')), '2023-09-01').dnsSettings.fqdn)]"
    }
  }
}        

Don't be overwhelmed by this extensive block of code—I've sourced it directly from Microsoft's official documentation, making only minor adjustments to suit our specific scenario. The core structure remains unchanged, so it's reliable and aligns closely with best practices recommended by Microsoft.

The template sets up the following components:

? Virtual Machine (Ubuntu): Ubuntu 20.04 or 22.04 VM.

? Virtual Network (VNet): To manage VM network traffic.

? Subnet: To segment network resources.

? Public IP: Enables external SSH access.

? Network Interface (NIC): Connects VM to the subnet and public IP.

? Network Security Group (NSG): Security rules (e.g., SSH access).

? Secure Boot / Trusted Launch: Optional security enhancements (TPM, secure boot).

? Extension (Guest Attestation): If Trusted Launch is enabled.

In Azure Resource Manager (ARM) templates, parameters are values that you can define and pass into the template during deployment, allowing you to customize resources dynamically without changing the template itself.

Why Use Parameters?

  • Flexibility: Allows you to use a single template to deploy resources across multiple environments (e.g., dev, staging, prod).
  • Reusability: Enables you to reuse templates without needing to modify the core template each time.
  • Security: Separates sensitive data (e.g., passwords, keys) from template code.

In this step, I've updated the VM name to "ubuntu-vm" for clearer identification and set the default value of "authenticationType" to "sshPublicKey". Using SSH public key authentication is strongly recommended as it offers enhanced security compared to traditional password-based logins, significantly reducing the risk of unauthorized access to your virtual machine.


Additionally, I've updated the ubuntuOsVersion parameter to reference the "Ubuntu-2204" value, which I've defined earlier in the variables section of the ARM template. Centralizing OS version details in the variables section helps simplify future updates and ensures consistency across your resources. This practice promotes maintainability and clarity in your template structure.


I've chosen the Standard B1s virtual machine type as it provides an optimal balance between performance and cost-effectiveness, making it an ideal choice for our use case. This VM size is particularly suitable for workloads that don't require continuous high performance, helping us efficiently manage costs while maintaining the necessary level of performance for our application.


With our ARM template now ready, it's time to deploy the resources we've defined. Let's proceed with deploying the ARM template to Azure, which will provision our resources seamlessly and efficiently, ensuring everything is configured exactly as specified in our infrastructure-as-code template.

Now, let's deploy our virtual machine using the Azure CLI. Execute the following command to start provisioning resources based on the ARM template we've created:

az deployment group create \
  --resource-group <resource-group-name> \
  --template-file azuredeploy.json \
  --parameters \
   adminUsername=<username> \
   adminPasswordOrKey='<ssh-public-key-or-password>'        

Remember to replace the placeholders <resource-group-name>, <username>, and <ssh-public-key-or-password> with your actual values. Using parameters ensures flexibility and allows secure input of sensitive information, like your SSH public key or password, without directly embedding them in the template file.

  az deployment group create \
  --resource-group rg-linux-vm \
  --template-file ubuntu-vm.json \
  --parameters \
   adminUsername=azureuser \
   adminPasswordOrKey="$(cat ~/.ssh/id_rsa.pem.pub)"        

Once your resources are successfully deployed, you'll see a confirmation message similar to the following in the Azure CLI output, indicating that your deployment was completed without issues. This message confirms that your resources are now up and running in Azure, configured exactly as defined in your ARM template.


In the next post, we'll explore how to securely log in to our newly deployed virtual machine, configure Git, and deploy an ASP.NET Core application step-by-step. Stay tuned for practical tips and clear instructions to smoothly manage your deployment process. Until then, happy coding!


要查看或添加评论,请登录

Sasanga Edirisinghe的更多文章

社区洞察

其他会员也浏览了