Azure Bastion を Terraform で構築

## はじめに
Azure Portal からウェブブラウザを通して VM へのアクセスを提供する、[Azure Bastion](https://azure.microsoft.com/ja-jp/services/azure-bastion/#overview) がある
Public IP を持たない VM へのアクセス手段にもなる
Azure Bastion の概要を調べ、Terraform でコード化して構築したのでその内容を記載する

![スクリーンショット 2022-04-09 12.42.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/46b17b8f-fead-144f-1eae-523629cd8805.png)


## Azure Bastion
[Azure Bastion ドキュメント](https://docs.microsoft.com/ja-jp/azure/bastion/bastion-overview)から抜粋

- Azure portal を使用して仮想マシンに接続できるようにするサービス
    - ユーザの仮想ネットワーク内でプロビジョニングする、フル プラットフォーム マネージド PaaS サービス
- Azure portal から直接、TLS 経由で仮想マシンに RDP/SSH 接続
    - 接続先の仮想マシンにパブリック IP アドレス、エージェント、クライアント ソフトウェアはいずれも不要

利点は[ドキュメント](https://docs.microsoft.com/ja-jp/azure/bastion/bastion-overview#key)に表でまとまっているのでそちらを参照

### サブネット
Azure Bastion は指定された名前で指定されたサイズ以上で Subnet を作成する必要がある ([ドキュメント](https://docs.microsoft.com/ja-jp/azure/bastion/configuration-settings#subnet))

- サブネット名: **AzureBastionSubnet**
- サイズ    : **/26** 以上


### NSG (Network Security Group)

AzureBastionSubnet への NSG 設定は、指定されたルールを設定しないと設定ができない
必要なルールは[ドキュメント](https://docs.microsoft.com/ja-jp/azure/bastion/bastion-nsg#apply)から抜粋したものが下記の通り

Ingress Traffic

| 内容 | Port | Protocol | Source | Destination |
|:-:|:-:|:-:|:-:|:-:|
| パブリック インターネットからの Ingress トラフィック | 443 | Tcp | Internet <br>or 指定したパブリック IP アドレスのセット | Any |
| Azure Bastion からの Ingress トラフィックのコントロール プレーン | 443 | Tcp | GatewayManager | Any |
| Azure Load Balancer からの Ingress トラフィック | 443 | Tcp | AzureLoadBalancer | Any |
| Azure Bastion データ プレーンからの Ingress トラフィック | 8080, 5701 | Any | VirtualNetwork | VirtualNetwork |

Egress Traffic

| 内容 | Port | Protocol | Source | Destination |
|:-:|:-:|:-:|:-:|:-:|
| ターゲット VM への Egress トラフィック | 22, 3389 | Any | Any |  VirtualNetwork |
| Azure の他のパブリックエンド ポイントへの Egress トラフィック | 443 | Tcp | Any | AzureCloud |
| Azure Bastion データ プレーンへの Egress トラフィック | 8080, 5701 | Any | VirtualNetwork | VirtualNetwork |
| インターネットへの Egress トラフィック | 80 | Any | Any | Internet |


### SKU / コスト
SKU は `Basic` と `Standard` がある ([ドキュメント](https://docs.microsoft.com/ja-jp/azure/bastion/configuration-settings#skus))
価格を [Azure サイト](https://azure.microsoft.com/ja-jp/pricing/details/azure-bastion/)から抜粋したもの (2022.04.09 時点 / 1 USD = 122.71 JPY)

| SKU | 料金/時間 | 料金/月(31日) | 機能 |
|:--|:--|:--|:--|
| Basic | ¥23.315 | ¥17,346.36 | ・ピアリングされた仮想ネットワーク内のターゲット VM に接続する<br>・Azure Key Vault (AKV) で Linux VM のプライベート キーにアクセスする<br>・SSH を使用した Linux VM への接続<br>・RDP を使用した Windows VM への接続 |
| Standard | ¥35.586 | ¥26,475.984 | Basic の機能に加えて下記の機能が追加される<br>・ホストのスケーリング (料金は2インスタンス含. それ以上の追加は ¥17.180 / 時間)<br>・カスタム受信ポートの指定<br>・RDP を使用した Linux VM への接続<br>・SSH を使用した Windows VM への接続<br>・ファイルのアップロードまたはダウンロード |

追加で送信データ転送コストがかかる ([ドキュメント](https://azure.microsoft.com/ja-jp/pricing/details/azure-bastion/)参照. 最初の 5 GB/月は無料)

ひと月で約1万7千円以上かかるので、個人利用の場合は注意が必要

### 対応ブラウザ
2022.04.09 時点の [FAQ ページ](https://docs.microsoft.com/ja-jp/azure/bastion/bastion-faq#browsers)より抜粋

- HTML 5 をサポートしている必要がある
- Windows : Microsoft Edge または Google Chrome
- Mac : Google Chrome

### IPv6 非サポート
2022.04.09 時点、サポートされていない ([FAQ](https://docs.microsoft.com/ja-jp/azure/bastion/bastion-faq#is-ipv6-supported)から抜粋)

## Terraform での構築・テスト
実際に Terraform でコード化して、テスト構築して利用テストをしたので、
サンプルコードと試した内容を記載する

### 前提
Azure Bastion を構築する上で下記構築済み
- Azure 環境は構築済み
- VNET や VM は構築済み

また、下記も Hub Spoke で Hub に Bastion を作成して Spoke の VM にアクセスできることを確認するため実施済み
- Hub-Spoke 構成を構築済み

上記の構築の詳細は別 Qiita 記事で記載している

https://qiita.com/suzuyui/items/2eca3dd3937d64c40fd4

Terraform Version は下記の通り
- terraform : v1.1.7
- provider registry.terraform.io/hashicorp/azurerm : v3.1.0

### 構成
下記構成で構築・試している
Hub/Spoke 構成で、Hub 側に Azure Bastion を構築して、Spoke 側の VM へ Azure Portal 経由でアクセスする

![AzureBastion構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/552a6a53-c71b-c6fe-d608-96f820c3e2cb.png)


### Terraform コード
Azure Bastion を作成するコードを module 化したコードを記載する

`Azure Bastion` と Azure Bastion が所属する`AzureBastionSubnet`向けの NSG 設定をコード化している

#### module 利用コード
記載する module コードの利用コード例が下記の通り

- hostname
    - Azure Bastion の名前
- location
    - Azure Bastion を設置するリージョン
- resource_group_name
    - Azure Bastion を設置するリソースグループ名
- subnet_id
    - Azure Bastion を設置する VNET で作成した Subnet ID (作成サブネット名は AzureBastionSubnet)
- nsg_public_source_ips
    - ウェブブラウザで Azure Portal からアクセスする送信元の PC の Public IP のリスト

```tf
module "hub-azure-batsion" {
  source = "../../../../../modules/hub/azure-bastion"

  hostname              = "bas-testinfra-japaneast-001"
  location              = "japaneast"
  resource_group_name   = "rg-hub-testinfra-japaneast-001"
  subnet_id             = "/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/rg-hub-testinfra-japaneast-001/providers/Microsoft.Network/virtualNetworks/vnet-testinfra-japaneast-001/subnets/AzureBastionSubnet"
  nsg_public_source_ips = ["x.x.x.x/32(HomeLabo Public IP)", ]
}

```

#### module コード
Terraform コード例は下記の通り
- variables.tf : 設定する変数
- main.tf : リソース定義

```tf:modules/hub/azure-bastion/variables.tf
variable "resource_group_name" {}

variable "location" {}

variable "hostname" {}

variable "subnet_id" {} # https://docs.microsoft.com/ja-jp/azure/bastion/configuration-settings#subnet /26 以上

# https://docs.microsoft.com/ja-jp/azure/bastion/configuration-settings#skus
##  Basic -> Standard 可能 https://docs.microsoft.com/ja-jp/azure/bastion/upgrade-sku
variable "sku" {
  type    = string
  default = "Basic" # Basic or Standard
}

# azurerm version 2.93.0 以上が必須
variable "copy_paste_enabled" {
  type    = bool
  default = true
}

# azurerm version 2.93.0 以上が必須
# sku = "Standard" が必須
variable "file_copy_enabled" {
  type    = bool
  default = true
}

# azurerm version 2.93.0 以上が必須
# sku = "Standard" が必須
variable "scale_units" {
  type    = number
  default = 2
}

variable "nsg_public_source_ips" {
  type    = list(string)
  default = null
}

```

```tf:modules/hub/azure-bastion/main.tf
# NSG / https://docs.microsoft.com/ja-jp/azure/bastion/bastion-nsg
resource "azurerm_network_security_group" "nsg" {
  name                = "nsg-azurebastion-001"
  location            = var.location
  resource_group_name = var.resource_group_name
}

resource "azurerm_network_security_rule" "ingress-rule-1" {
  name                        = "AllowHttpsInbound"
  priority                    = 1000
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_ranges     = ["443", ]
  source_address_prefix       = var.nsg_public_source_ips == null ? "Internet" : null
  source_address_prefixes     = var.nsg_public_source_ips == null ? null : var.nsg_public_source_ips
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_network_security_group.nsg.resource_group_name
  network_security_group_name = azurerm_network_security_group.nsg.name
}

resource "azurerm_network_security_rule" "ingress-rule-2" {
  name                        = "AllowGatewayManagerInbound"
  priority                    = 1010
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_ranges     = ["443", ]
  source_address_prefix       = "GatewayManager"
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_network_security_group.nsg.resource_group_name
  network_security_group_name = azurerm_network_security_group.nsg.name
}

resource "azurerm_network_security_rule" "ingress-rule-3" {
  name                        = "AllowAzureLoadBalancerInbound"
  priority                    = 1020
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_ranges     = ["443", ]
  source_address_prefix       = "AzureLoadBalancer"
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_network_security_group.nsg.resource_group_name
  network_security_group_name = azurerm_network_security_group.nsg.name
}

resource "azurerm_network_security_rule" "ingress-rule-4" {
  name                        = "AllowBastionHostCommunicationInbound"
  priority                    = 1030
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "*"
  source_port_range           = "*"
  destination_port_ranges     = ["8080", "5701", ]
  source_address_prefix       = "VirtualNetwork"
  destination_address_prefix  = "VirtualNetwork"
  resource_group_name         = azurerm_network_security_group.nsg.resource_group_name
  network_security_group_name = azurerm_network_security_group.nsg.name
}


resource "azurerm_network_security_rule" "ingress-rule-deny" {
  name                        = "DenyAllInbound"
  priority                    = 4096
  direction                   = "Inbound"
  access                      = "Deny"
  protocol                    = "*"
  source_port_range           = "*"
  destination_port_range      = "*"
  source_address_prefix       = "*"
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_network_security_group.nsg.resource_group_name
  network_security_group_name = azurerm_network_security_group.nsg.name
}

resource "azurerm_network_security_rule" "egress-rule-1" {
  name                        = "AllowSshRdpOutbound"
  priority                    = 1000
  direction                   = "Outbound"
  access                      = "Allow"
  protocol                    = "*"
  source_port_range           = "*"
  destination_port_ranges     = ["22", "3389", ]
  source_address_prefix       = "*"
  destination_address_prefix  = "VirtualNetwork"
  resource_group_name         = azurerm_network_security_group.nsg.resource_group_name
  network_security_group_name = azurerm_network_security_group.nsg.name
}

resource "azurerm_network_security_rule" "egress-rule-2" {
  name                        = "AllowAzureCloudOutbound"
  priority                    = 1010
  direction                   = "Outbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_ranges     = ["443", ]
  source_address_prefix       = "*"
  destination_address_prefix  = "AzureCloud"
  resource_group_name         = azurerm_network_security_group.nsg.resource_group_name
  network_security_group_name = azurerm_network_security_group.nsg.name
}

resource "azurerm_network_security_rule" "egress-rule-3" {
  name                        = "AllowBastionHostCommunicationOutbound"
  priority                    = 1020
  direction                   = "Outbound"
  access                      = "Allow"
  protocol                    = "*"
  source_port_range           = "*"
  destination_port_ranges     = ["8080", "5701", ]
  source_address_prefix       = "VirtualNetwork"
  destination_address_prefix  = "VirtualNetwork"
  resource_group_name         = azurerm_network_security_group.nsg.resource_group_name
  network_security_group_name = azurerm_network_security_group.nsg.name
}

resource "azurerm_network_security_rule" "egress-rule-4" {
  name                        = "AllowGetSessionInfomation"
  priority                    = 1030
  direction                   = "Outbound"
  access                      = "Allow"
  protocol                    = "*"
  source_port_range           = "*"
  destination_port_ranges     = ["80", ]
  source_address_prefix       = "*"
  destination_address_prefix  = "Internet"
  resource_group_name         = azurerm_network_security_group.nsg.resource_group_name
  network_security_group_name = azurerm_network_security_group.nsg.name
}

resource "azurerm_network_security_rule" "egress-rule-deny" {
  name                        = "DenyAllOutbound"
  priority                    = 4096
  direction                   = "Outbound"
  access                      = "Deny"
  protocol                    = "*"
  source_port_range           = "*"
  destination_port_range      = "*"
  source_address_prefix       = "*"
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_network_security_group.nsg.resource_group_name
  network_security_group_name = azurerm_network_security_group.nsg.name
}

resource "azurerm_subnet_network_security_group_association" "nsg" {
  subnet_id                 = var.subnet_id
  network_security_group_id = azurerm_network_security_group.nsg.id
  depends_on = [
    azurerm_network_security_rule.ingress-rule-1,
    azurerm_network_security_rule.ingress-rule-2,
    azurerm_network_security_rule.ingress-rule-3,
    azurerm_network_security_rule.ingress-rule-4,
    azurerm_network_security_rule.ingress-rule-deny,
    azurerm_network_security_rule.egress-rule-1,
    azurerm_network_security_rule.egress-rule-2,
    azurerm_network_security_rule.egress-rule-3,
    azurerm_network_security_rule.egress-rule-4,
    azurerm_network_security_rule.egress-rule-deny,
  ]
}

# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/bastion_host
resource "azurerm_public_ip" "main" {
  name                = "pip-bas-${var.location}-001"
  location            = var.location
  resource_group_name = var.resource_group_name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_bastion_host" "main" {
  name                = var.hostname
  location            = var.location
  resource_group_name = var.resource_group_name
  sku                 = var.sku
  copy_paste_enabled  = var.copy_paste_enabled
  file_copy_enabled   = var.sku == "Basic" ? null : var.file_copy_enabled
  scale_units         = var.sku == "Basic" ? null : var.scale_units

  ip_configuration {
    name                 = "configuration"
    subnet_id            = var.subnet_id
    public_ip_address_id = azurerm_public_ip.main.id
  }
  depends_on = [
    azurerm_subnet_network_security_group_association.nsg,
  ]
}

```

### 構築後のアクセステスト

#### Linux VM への接続テスト
仮想マシンの「接続」から`Bastion`を選択する

![スクリーンショット 2022-04-09 12.42.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/46b17b8f-fead-144f-1eae-523629cd8805.png)

`ユーザー名`と、`認証の種類`と必要な項目を入力して`接続`をクリックする

![スクリーンショット 2022-04-09 14.49.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/935c5b3e-02a4-bb47-61e0-8796f16498df.png)

上記実施すると、下記のようなタブが追加され HTML5 で操作可能

![スクリーンショット 2022-04-09 14.52.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/8a5313d6-70b5-140c-647a-e14ab65148a2.png)



NSG で送信元を制限する場合、許可してない IP からアクセス時は下記のようにエラーとなる

![スクリーンショット 2022-04-09 15.04.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/ef30fd89-f62f-8888-b404-c5685cb8477b.png)

#### Windows VM への接続テスト
仮想マシンの「接続」から`Bastion`を選択して下記画面を表示して、
`ユーザー名`と`パスワード`を入力して、`接続`をクリックする

![スクリーンショット 2022-04-09 18.17.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/9d3772fa-ad5b-74a3-8e76-e43b4c6dafed.png)

ログインが始まり、操作可能になる

![スクリーンショット 2022-04-09 18.13.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/fd465a2d-cf64-ee08-5763-7008cac4ee1a.png)
![スクリーンショット 2022-04-09 18.14.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/ed5a7d5c-6435-452f-7d8c-e3e8d86f79fb.png)

以上、テスト終了。

## おわりに
Azure Bastion の作成とテストまでを実施した
Terraform 化したので必要時に `terraform apply` で作成して、不要時は金額が高額なので`terraform destroy`で削除しておくのが、個人ラボ利用としては良さそう

## 参考

https://azure.microsoft.com/ja-jp/services/azure-bastion/#overview

https://docs.microsoft.com/ja-jp/azure/bastion/bastion-overview

https://docs.microsoft.com/ja-jp/azure/bastion/configuration-settings

https://docs.microsoft.com/ja-jp/azure/bastion/bastion-nsg

https://azure.microsoft.com/ja-jp/pricing/details/azure-bastion/

https://docs.microsoft.com/ja-jp/azure/bastion/bastion-faq

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/bastion_host