【Terraform】 Azure Bastionを使ってAzure Virtual Machineに接続してみる

【Terraform】 Azure Bastionを使ってAzure Virtual Machineに接続してみる

Clock Icon2025.05.08

はじめに

こんにちは、コンサルティング部の神野です。

Azure環境では、踏み台サーバーを自前で構築・運用するのではなく、マネージドサービスである「Azure Bastion」を活用することでAzure Virtual Machine(以下VM)に対してセキュアに接続することを可能とします。

今回は、Terraformを使ってAzure BastionとVMを構築し、簡単に安全なリモートアクセス環境を作る方法をご紹介します。「とりあえず試してみたい」という方にも参考になるよう、シンプルな構成で試してみました。

Azure Bastionとは

Azure Bastionは、Azureが提供するマネージドサービスで、仮想マシンへの安全なリモートアクセスを実現するためのサービスです。従来のように踏み台サーバーを自前で構築・運用する必要がなく、AzureポータルからブラウザベースでSSHやRDP接続が可能になります。

主な特徴としては、仮想マシン自体にパブリックIPを割り当てる必要がなくインターネットに直接公開されないセキュアなアクセスが可能であることや、追加のクライアントソフトウェアやエージェントのインストールが不要なブラウザベースの接続を提供していることが挙げられます。さらに、特定のポート(SSH:22、RDP:3389など)をインターネットに公開する必要がないため、Azure Network Security Group(以下NSG)の設定が簡素化されるというメリットもあります。

https://azure.microsoft.com/ja-jp/products/azure-bastion

Azure Bastionには、各種SKUのサービスレベルがあります。Standard SKUでは、ポート転送やファイル転送などの高度な機能が利用できますが、その分コストも高くなります。(補足に各SKUの比較を記載しておりますので、必要に応じてご参照ください)

今回は、より柔軟性の高いStandard SKUを使用して環境を構築していきます。

今回構築する環境

今回は以下のようなシンプルな環境を構築します。

  • Azure Bastion(Standard SKU、トンネリング有効)
    • Azure BastionにアタッチするパブリックIPアドレス
  • 仮想ネットワーク
  • サブネット(Bastion用とVM用)
  • 仮想マシン(Ubuntu 22.04 LTS)
  • VM用ネットワークセキュリティグループ
    • Bastionからの接続のみを許可(Port:22)
    • インターネットからの接続を全て拒否

構成のイメージ図

CleanShot 2025-05-08 at 00.35.37@2x

前提条件

  • Terraformがインストールされていること(v1.0.0以上推奨)

    • 使用したバージョン:Terraform v1.11.4 on darwin_arm64
  • Azure CLIがインストールされ、認証済みであること

    • 使用したバージョン:azure-cli 2.70.0
  • 自身の権限が適応できるAzureサブスクリプション・リソースグループを保持していること

SSH鍵の準備

今回のコードでは、仮想マシンへのSSH接続のために公開鍵認証を使用します。
SSH鍵を用意していない場合は、以下のコマンドで生成してください。
下記ではazure_keyといった名前で作成しています。任意の名前をお使いください。

# SSH鍵を生成(パスフレーズなし)
ssh-keygen -t rsa -b 2048 -f ~/.ssh/azure_key -N ""

# 鍵が生成されたことを確認
ls -la ~/.ssh/azure_key*

Terraformコードの作成

まずは、必要なファイルを作成していきます。今回は以下の3つのファイルを用意します。

  • main.tf

    • プロバイダーの設定
  • network.tf

    • ネットワークリソースの定義
  • vm.tf

    • 仮想マシンの定義

サクッと作成することに主眼を置いているため、一部おざなりになっている箇所もあるかと思います。実際のワークロードや設計方針に合わせて修正ください。

main.tf

サブスクリプションとリソースグループは存在するため、すでに存在するサブスクリプションのIDとdataブロックでリソースグループは参照します。変数などで定義するもいいかと思いますし、リソースグループが存在しない場合は作成するのもいいかと思います。

main.tf
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.27.0"
    }
  }
  required_version = ">= 1.0"
}

provider "azurerm" {
  features {}
  subscription_id = "任意のサブスクリプションID"
}

data "azurerm_resource_group" "rg" {
  name = "任意のリソースグループ名"
}

network.tf

サブネットを2つ作成します。Azure Bastion用サブネットとVM用のサブネットです。
BastionにはPublic IPを付与して外部からのアクセスを可能とし、VM用のNSGはBastionサブネットからの22ポート(SSH)へのアクセスのみを許可し、インターネットからの直接アクセスは拒否する設定にします。これによってVM環境のセキュリティを確保しつつ、Bastionを経由した安全なアクセスのみを許可する構成とします。

BastionのSKUはStandardとしています。ローカル端末のAzure CLIからのアクセスも実施したいためです。

またAzureBastionSubnetにTLSのみを許可するNSGもアタッチすることが可能ですが今回は省略しております。
もしNSGを設定される場合は下記ドキュメントに従ってポートを開放する設定にしてください。

https://learn.microsoft.com/ja-jp/azure/bastion/bastion-nsg#apply

network.tf
resource "azurerm_virtual_network" "vnet" {
  name                = "bastion-vnet"
  address_space       = ["10.0.0.0/16"]
  location            = data.azurerm_resource_group.rg.location
  resource_group_name = data.azurerm_resource_group.rg.name
}

resource "azurerm_subnet" "vm_subnet" {
  name                 = "vm-subnet"
  resource_group_name  = data.azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.1.0/24"]
}

resource "azurerm_subnet" "bastion_subnet" {
  name                 = "AzureBastionSubnet" # この名前は固定である必要があります
  resource_group_name  = data.azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.0.0/24"]
}

resource "azurerm_public_ip" "bastion_pip" {
  name                = "bastion-pip"
  location            = data.azurerm_resource_group.rg.location
  resource_group_name = data.azurerm_resource_group.rg.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_bastion_host" "bastion" {
  name                = "demo-bastion"
  sku                 = "Standard"
  tunneling_enabled   = true
  location            = data.azurerm_resource_group.rg.location
  resource_group_name = data.azurerm_resource_group.rg.name

  ip_configuration {
    name                 = "configuration"
    subnet_id            = azurerm_subnet.bastion_subnet.id
    public_ip_address_id = azurerm_public_ip.bastion_pip.id
  }
}

resource "azurerm_network_security_group" "vm_nsg" {
  name                = "vm-nsg"
  location            = data.azurerm_resource_group.rg.location
  resource_group_name = data.azurerm_resource_group.rg.name

  # Bastionからの22番ポートへのアクセスを許可
  security_rule {
    name                       = "allow-ssh-from-bastion"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "10.0.0.0/24"  # Bastionサブネットのアドレス範囲
    destination_address_prefix = "*"
  }

  # 外部からの直接アクセスは全て拒否
  security_rule {
    name                       = "deny-inbound-all"
    priority                   = 1000
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "Internet"
    destination_address_prefix = "*"
  }
}

resource "azurerm_subnet_network_security_group_association" "vm_nsg_association" {
  subnet_id                 = azurerm_subnet.vm_subnet.id
  network_security_group_id = azurerm_network_security_group.vm_nsg.id
}

tunneling_enabledはローカル端末からAzure CLIを使ってネイティブ クライアント機能を実施するために有効にしています。

vm.tf

こちらはVMの実装を書いています。internalとしてPrivateIPのみ付与しています。
また、今回は手順の簡略化のために事前に作成したローカルファイルの公開鍵を直接登録する仕組みとしています。

vm.tf
resource "azurerm_network_interface" "vm_nic" {
  name                = "vm-nic"
  location            = data.azurerm_resource_group.rg.location
  resource_group_name = data.azurerm_resource_group.rg.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.vm_subnet.id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_linux_virtual_machine" "vm" {
  name                = "demo-vm"
  resource_group_name = data.azurerm_resource_group.rg.name
  location            = data.azurerm_resource_group.rg.location
  size                = "Standard_B1s"
  admin_username      = "azureuser"
  network_interface_ids = [
    azurerm_network_interface.vm_nic.id,
  ]

  admin_ssh_key {
    username   = "azureuser"
    public_key = file("~/.ssh/azure_key.pub")  # ローカルのSSH公開鍵ファイル
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }
}

デプロイ手順

作成したTerraformコードをデプロイするには、以下の手順で実行します。

  1. まず、Terraformの初期化を行います。
terraform init
  1. 作成されるリソースを確認します。
terraform plan
  1. リソースをデプロイします。
terraform apply

yesと入力して、デプロイを開始します。デプロイには10〜15分程度かかることがあります。特にAzure Bastionのプロビジョニングには時間がかかりますので、少々お待ちください。

Azure Bastionを使ってVMに接続する

Azure Portalから接続

デプロイが完了したら、Azure Portalから仮想マシンに接続してみましょう。

  1. Azure Portalにログインします。

  2. 作成した仮想マシン(demo-vm)のページに移動します。

  3. 「接続」ボタンをクリックし、「Bastion」を選択します。

  4. 認証方法として「SSH秘密キー」を選択し、以下の情報を入力します。

  • ユーザー名: azureuser
  • 認証タイプ: SSH秘密キー
  • 秘密キーファイル: 生成した秘密鍵(~/.ssh/azure_key)を選択

CleanShot 2025-05-07 at 22.58.25@2x

  1. 「接続」をクリックすると、ブラウザ上でSSHセッションが開始されます。
    CleanShot 2025-05-07 at 22.59.25@2x

これで、インターネットに公開されたポートを持たない仮想マシンにアクセスできました!

ローカル端末のAzure CLIからアクセス

一方でAzure CLIからもローカル上の秘密鍵を使用してSSHで接続することが可能です。
下記コマンドを実行します。

コマンド引数の説明

引数 説明
--name 接続に使用するAzure Bastionホストの名前を指定します。例: "demo-bastion"
--resource-group Azure BastionホストとVMが属するリソースグループの名前を指定します。<your-resource-group-name> を実際の名前に置き換えてください。
--target-resource-id 接続先の仮想マシンのリソースIDを指定します。Azure PortalでVMのプロパティから確認できます。<your-vm-resource-id> を実際のIDに置き換えてください。
--auth-type 認証方法を指定します。SSHキー認証の場合は "ssh-key" を指定します。
--username 仮想マシンにSSH接続する際のユーザー名を指定します。例: "azureuser"
--ssh-key SSH接続に使用する秘密鍵ファイルのパスを指定します。例: "~/.ssh/azure_key"

実行コマンド

Azure CLIの実行コマンド
az network bastion ssh \
    --name "demo-bastion" \
    --resource-group "<your-resource-group-name>" \
    --target-resource-id "<your-vm-resource-id>" \
    --auth-type "ssh-key" \
    --username "azureuser" \
    --ssh-key "~/.ssh/azure_key"

実行結果

CleanShot 2025-05-07 at 23.01.43@2x-6626552

こちらも問題なく接続できていますね!Azure CLIでローカル上からアクセスできるのも便利ですね。

ハマりポイントと注意点

一部特に気になった箇所を記載しました。より詳細な注意点は公式ドキュメントに記載があるので、必要に応じてご参照ください。

https://learn.microsoft.com/ja-jp/azure/bastion/configuration-settings#subnet

Bastionサブネット名は固定

Azure Bastionを配置するサブネットの名前は必ずAzureBastionSubnetである必要があります。これは、Azureの仕様によるものです。名前を変更するとデプロイに失敗しますので注意してください。

サブネットのサイズ

Bastionサブネットは少なくとも/26以上のサイズ(最低64個のIPアドレス)が必要です。今回は余裕を持って/24(256個のIPアドレス)を割り当てています。

パブリックIPのSKU

Azure BastionはStandard SKUのパブリックIPアドレスを要求します。Basic SKUを指定するとエラーになりますので注意してください。

削除について

Azure Bastionは便利なサービスですが、常時稼働させると月額で数千円〜1万円程度のコストがかかります。検証環境や一時的な利用の場合は、使用後にterraform destroyコマンドでリソースを削除することをお勧めします。

terraform destroy

おわりに

今回は、Terraformを使ってAzure Bastionと仮想マシンを構築し、セキュアなリモートアクセス環境を簡単に作成する方法をご紹介しました。

Azure Bastionを使うことで、踏み台サーバーの構築・運用が不要になり、仮想マシンにパブリックIPを割り当てる必要もなくなります。また、SSHポートを外部に公開する必要がないためセキュリティが向上し、ブラウザから直接SSH接続が可能になるため利便性も高まります。

セキュリティを向上させつつ、運用の手間を減らせるAzure Bastionは、多くのシナリオで有用なサービスだと思います。ぜひ、皆さんも試してみてください。

今回のコードはシンプルな例ですが、実際の環境では複数のVMを管理したり、より複雑なネットワーク構成が必要になることもあるかと思いますが、少しでも参考になりましたら幸いです。

最後までお読みいただき、ありがとうございました!

補足 :Azure BastionのSKU比較

Azure Bastionには、Developer、Basic、Standard、Premiumの4つのSKUがあり、それぞれ利用できる機能や料金が異なります。

各SKUの主な特徴と比較

機能/特徴 Developer SKU Basic SKU Standard SKU Premium SKU
料金 無料 有料 有料 有料
同じ仮想ネットワーク内のVM接続 可能 可能 可能 可能
ピアリングされた仮想ネットワーク内のVM接続 不可 可能 可能 可能
ホストのスケーリング 不可 不可 可能 可能
ファイルのアップロード/ダウンロード 不可 不可 可能 可能
カスタム受信ポートの指定 不可 不可 可能 可能
Azure CLIを使用したVM接続 不可 不可 可能 可能
共有可能リンク 不可 不可 可能 可能
IPアドレスベースの接続 不可 不可 可能 可能
ネイティブクライアントサポート 不可 不可 可能 可能
セッションの記録 不可 不可 不可 可能
プライベート専用デプロイ 不可 不可 不可 可能
利用可能なリージョン 一部限定 制限なし 制限なし 制限なし
Bastion専用サブネット 不要 必要 必要 必要

SKU選択のポイント

  • Developer SKU: 無料で基本的なSSH/RDP接続機能を利用したい場合に適していますが、同じ仮想ネットワーク内のVMにしか接続できず、利用可能なリージョンも限定されています。開発/テスト環境のコスト削減に役立ちます。
  • Basic SKU: ピアリングされた仮想ネットワーク内のVMへの接続が必要な場合の基本的な選択肢です。
  • Standard SKU: ホストのスケーリング、ファイルのアップロード/ダウンロード、ネイティブクライアント接続、共有可能リンク、IPアドレスベースの接続など、より高度な機能が必要な場合に選択します。
  • Premium SKU: Standard SKUの機能に加え、セッション記録やプライベート専用のデプロイといった、セキュリティやコンプライアンス要件が厳しい場合に適しています。

SKUは後からアップグレードできますが、ダウングレードはサポートされておらず、その場合はBastionを削除して再作成する必要があります。
公式ドキュメントにも情報が記載されているので、必要に応じてご参照ください。
https://learn.microsoft.com/ja-jp/azure/bastion/configuration-settings#skus

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.