Deploy a Private Azure Cloud Shell with Terraform

By default Cloud Shell sessions run inside a container inside a Microsoft network separate from any resources you may have deployed in Azure. So what happens when you want to access services you have deployed inside a Virtual Network such as a private AKS cluster, a Virtual Machine or Private Endpoint enabled services?

Well as you can imagine the solution is to deploy Cloud Shell into an Azure Virtual Network in such a way that the container, it runs on, can acces your private resources. This solution also protects the backing Storage Account that Cloud Shell uses for you the user profiles and data so that you end up with a locked down environmemt.

The following diagram shows the solution archictecture:

Cloud Shell in an Azure Virtual Network Architecture Diagram

If you need more information about the solution, the services and any limitations please check the documentation here: Cloud Shell in an Azure Virtual Network .

Now let’s se how can we use terraform to make your Cloud Shell private!

Deploy a Private Azure Cloud Shell with Terraform

If Cloud Shell has been used in the past, the existing clouddrive must be unmounted. To do this run clouddrive unmount from an active Cloud Shell session.

1. Create a provider.tf file with the following contents:

 1terraform {
 2  required_version = ">= 0.13.5"
 3}
 4
 5provider "azurerm" {
 6  version = "= 2.46.1"
 7  features {}
 8}
 9
10provider "azuread" {
11  version = "= 1.3.0"
12}
13
14provider "http" {
15  version = "= 2.0.0"
16}

Note that we are using azurerm to deploy Azure services, azuread to get some Service Principal information and http to get your current public ip address so that only you can reach your Cloud Shell.

2. Create a variables.tf file with the following contents:

 1variable location {
 2  default = "west europe"
 3}
 4
 5variable resource_group {
 6  default = "<resource group name>"
 7}
 8
 9variable "vnet_name" {
10  default = "<vnet name>"
11}
12
13variable sa_name {
14  default = "<Backing Storage Account name>"
15}
16
17variable relay_name {
18  default = "<Azure Relay name>"
19}

Make sure you replace the placeholders with the default values you want to use.

3. Create a main.tf file with the following contents:

  1# Get Azure Container Instance Service Principal. Amazing right? Cloud Shell uses this Service Principal!
  2data "azuread_service_principal" "container" {
  3  display_name = "Azure Container Instance Service"
  4}
  5
  6# Create Resource Groupto hold the resources.
  7resource "azurerm_resource_group" "rg" {
  8  name     = var.resource_group
  9  location = var.location
 10}
 11
 12# Create a VNET.
 13resource "azurerm_virtual_network" "vnet" {
 14  name                = var.vnet_name
 15  address_space       = ["10.0.0.0/16"]
 16  location            = azurerm_resource_group.rg.location
 17  resource_group_name = azurerm_resource_group.rg.name
 18}
 19
 20# Create a Containers Subnet. Here is where Cloud Shell will run.
 21resource "azurerm_subnet" "containers" {
 22  name                 = "cloudshell-containers"
 23  resource_group_name  = azurerm_resource_group.rg.name
 24  virtual_network_name = azurerm_virtual_network.vnet.name
 25  address_prefixes     = ["10.0.0.0/24"]
 26
 27  # Delegate the subnet to "Microsoft.ContainerInstance/containerGroups".
 28  delegation {
 29    name = "cloudshell-delegation"
 30
 31    service_delegation {
 32      name = "Microsoft.ContainerInstance/containerGroups"
 33    }
 34  }
 35
 36  # Add service enpoint so Cloud Shell can reach Storage Accounts. At the moment the solution does not work with Private Enpoints for the Storage Account. 
 37  service_endpoints = ["Microsoft.Storage"]
 38}
 39
 40# Create a subnet to host Azure Relay service.
 41resource "azurerm_subnet" "relay" {
 42  name                 = "relay"
 43  resource_group_name  = azurerm_resource_group.rg.name
 44  virtual_network_name = azurerm_virtual_network.vnet.name
 45  address_prefixes     = ["10.0.1.0/24"]
 46
 47  enforce_private_link_endpoint_network_policies = true
 48}
 49
 50# Create a network profile for the Cloud Shell containers.
 51resource "azurerm_network_profile" "networkprofile" {
 52  name                = "cloudshell"
 53  location            = azurerm_resource_group.rg.location
 54  resource_group_name = azurerm_resource_group.rg.name
 55
 56  container_network_interface {
 57    name = "cloudshell-containers"
 58
 59    ip_configuration {
 60      name      = "ipconfig"
 61      subnet_id = azurerm_subnet.containers.id
 62    }
 63  }
 64}
 65
 66# Assign Network Contributor to the Azure Container Instance Service Principal.
 67resource "azurerm_role_assignment" "network_contributor" {
 68  scope                = azurerm_network_profile.networkprofile.id
 69  role_definition_name = "Network Contributor"
 70  principal_id         = data.azuread_service_principal.container.object_id
 71}
 72
 73# Create an Azure Relay namespace.
 74resource "azurerm_relay_namespace" "relay" {
 75  name                = var.relay_name
 76  location            = azurerm_resource_group.rg.location
 77  resource_group_name = azurerm_resource_group.rg.name
 78
 79  sku_name = "Standard"
 80}
 81
 82# Add a private enpoint to the Azure Relay namespace.
 83resource "azurerm_private_endpoint" "endpoint" {
 84  name                = "cloudshell-privateendpoint"
 85  location            = azurerm_resource_group.rg.location
 86  resource_group_name = azurerm_resource_group.rg.name
 87  subnet_id           = azurerm_subnet.relay.id
 88
 89  private_service_connection {
 90    name                           = "privateendpoint"
 91    private_connection_resource_id = azurerm_relay_namespace.relay.id
 92    is_manual_connection           = false
 93    subresource_names              = ["namespace"]
 94  }
 95}
 96
 97# Assign Contributor to the Azure Container Instance Service Principal.
 98resource "azurerm_role_assignment" "contributor" {
 99  scope                = azurerm_relay_namespace.relay.id
100  role_definition_name = "Contributor"
101  principal_id         = data.azuread_service_principal.container.object_id
102}
103
104# Create the Storage Account to hold the Cloud Shell profiles.
105resource "azurerm_storage_account" "sa" {
106  name                      = var.sa_name
107  resource_group_name       = azurerm_resource_group.rg.name
108  location                  = azurerm_resource_group.rg.location
109  account_tier              = "Standard"
110  account_replication_type  = "GRS"
111  enable_https_traffic_only = true
112}
113
114# Create a file share to hold the user profiles.
115resource "azurerm_storage_share" "share" {
116  name                 = "profile"
117  storage_account_name = azurerm_storage_account.sa.name
118  quota                = 6
119}
120
121# Get your current public IP.
122data "http" "current_public_ip" {
123  url = "http://ipinfo.io/json"
124  request_headers = {
125    Accept = "application/json"
126  }
127}
128
129# Protect the Storage Account setting the firewall.
130# This is done only after the file share is created.
131resource "azurerm_storage_account_network_rules" "sa_rules" {
132  resource_group_name  = azurerm_resource_group.rg.name
133  storage_account_name = azurerm_storage_account.sa.name
134
135  default_action             = "Deny"
136  virtual_network_subnet_ids = [azurerm_subnet.containers.id]
137
138  # ip_rules = [
139  #   jsondecode(data.http.current_public_ip.body).ip
140  # ]
141
142  depends_on = [
143    azurerm_storage_share.share
144  ]
145}
146
147# Create DNS Zone for Relay
148resource "azurerm_private_dns_zone" "private" {
149  name                = "privatelink.servicebus.windows.net"
150  resource_group_name = azurerm_resource_group.rg.name
151}
152
153# Create A record for the Relay
154resource "azurerm_private_dns_a_record" "relay" {
155  name                = var.relay_name
156  zone_name           = azurerm_private_dns_zone.private.name
157  resource_group_name = azurerm_resource_group.rg.name
158  ttl                 = 3600
159  records             = [azurerm_private_endpoint.endpoint.private_service_connection[0].private_ip_address]
160}
161
162# Link the Private Zone with the VNet
163resource "azurerm_private_dns_zone_virtual_network_link" "relay" {
164  name                  = "relay"
165  resource_group_name   = azurerm_resource_group.rg.name
166  private_dns_zone_name = azurerm_private_dns_zone.private.name
167  virtual_network_id    = azurerm_virtual_network.vnet.id
168}
169
170# Open the relay firewall to local IP
171resource "null_resource" "open_relay_firewall" {
172  provisioner "local-exec" {
173    interpreter = ["powershell"]
174    command = "az rest --method put --uri '${azurerm_relay_namespace.relay.id}/networkrulesets/default?api-version=2017-04-01' --body '{\"properties\":{\"defaultAction\":\\\"Deny\\\",\"ipRules\":[{\"ipMask\":\\\"${jsondecode(data.http.current_public_ip.body).ip}\\\"}],\"virtualNetworkRules\":[],\"trustedServiceAccessEnabled\":false}}'"
175  }
176  depends_on = [
177    data.http.current_public_ip,
178    azurerm_relay_namespace.relay
179  ]
180}

4. Deploy the solution:

Run the following commands:

1terraform init
2terraform plan -out tf.plan
3terraform apply ./tf.plan

Hope it helps! Please find the complete code here


Extending Azure Functions with Dapr
Use Azure Landing Zones to rule them all
comments powered by Disqus