3大クラウド(AWS,GCP,Azure)でよくあるネットワーク構成を実践してみる

# はじめに
AWSでよく出てくる次のネットワーク構成。プライベートサブネットとパブリックサブネットを作成して、インターネットからアクセス可能なインスタンスとアクセス不可能なインスタンスを用意するやつです。
他のクラウドだと意外とこのような構成を実現する記事がなかったので、3大クラウド(AWS,GCP,Azure)で比較しながら、それぞれ実践してみました。
![aws_cloud_architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678160/35f58e6d-6acc-5f36-4132-da218215d9cf.png)

:::note info
筆者の本業は機械学習エンジニアであり、クラウドやインフラのエンジニアではないので、この構成を利用する際はあくまで自己責任でお願いします。また、もし不備等あればご指摘いただけるとありがたいです。
:::

# 作りたい構成の紹介
クラウドごとの差異もあるので、本構成として最低限必要な要素を洗い出してみます。

- インターネットからアクセス可能なネットワーク（インスタンス）が存在する
- インターネットからはアクセス不可能だが、インターネットに出ることができるネットワーク（インスタンス）が存在する
- ネットワークの分割はサブネット単位でなくても良い

※一般的にはパブリックサブネットにアプリケーションサーバー、プライベートサブネットにDBサーバーを置いたりすると思いますが、比較しやすいようにどちらもインスタンスとしています。
※ALBを利用する場合は、ALBをパブリックサブネットに配置すれば、アプリケーションサーバーもプライベートサブネットに配置できます。ただし、SSH接続を行うためには、パブリックサブネットにBastionを用意する必要があります。

# AWSでのネットワーク構成
構成図は冒頭で示した通りですが、以下のようになります。

![aws_cloud_architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678160/35f58e6d-6acc-5f36-4132-da218215d9cf.png)

構成図に現れない設定は以下の通りです
- パブリックサブネットのルートテーブルの設定で、デフォルトゲートウェイ(0.0.0.0/0)をInternet Gatewayにする
- プライベートサブネットのルートテーブルの設定で、デフォルトゲートウェイ(0.0.0.0/0)をパブリックサブネットのNAT Gatewayにする
- パブリックサブネットのインスタンスのパブリックIPの割り当てを有効化、　ファイアウォール（インバウンドルール）を適時設定
- プライベートサブネットのインスタンスのパブリックIPの割り当てを無効化

ちなみに、AWSのネットワーク周りの設定はこちらの動画がめちゃくちゃわかりやすいので、ぜひご覧ください

https://www.youtube.com/watch?v=7J_crzU5KP8

## 実践コード

<details><summary>backend.tf</summary>

```terraform:backend.tf
terraform {
  required_version = "~> 1.5.0"

  backend "local" {
    path = "./terraform.tfstate"
  }

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
```

</details>

<details><summary>valiable.tf</summary>

```terraform:valiable.tf
variable "region" {
  type    = string
  default = "ap-northeast-1"
}

variable "pub_key_path" {
  type    = string
  default = "~/.ssh/id_rsa.pub"
}
```

</details>

<details><summary>main.tf</summary>

```terraform:main.tf
# 利用する際は、mainをプロジェクト名に置き換えてください

provider "google" {
  project = var.project
  region  = var.region
}

# VPCとサブネットの設定
resource "google_compute_network" "vpc_main" {
  name                    = "vpc-main"
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "subnet_public_main" {
  name          = "subnet-public-main"
  ip_cidr_range = "10.0.1.0/24"
  region        = var.region
  network       = google_compute_network.vpc_main.name
}

resource "google_compute_subnetwork" "subnet_private_main" {
  name          = "subnet-private-main"
  ip_cidr_range = "10.0.2.0/24"
  region        = var.region
  network       = google_compute_network.vpc_main.name
}

# Cloud NATの設定
resource "google_compute_router" "router_main" {
  name    = "router-main"
  region  = var.region
  network = google_compute_network.vpc_main.name
}

resource "google_compute_router_nat" "nat_main" {
  name                               = "nat-main"
  router                             = google_compute_router.router_main.name
  region                             = var.region
  nat_ip_allocate_option             = "AUTO_ONLY"
  source_subnetwork_ip_ranges_to_nat = "LIST_OF_SUBNETWORKS"

  subnetwork {
    name                    = google_compute_subnetwork.subnet_private_main.name
    source_ip_ranges_to_nat = ["ALL_IP_RANGES"]
  }
}

# ファイアウォールの設定
resource "google_compute_firewall" "firewall_public" {
  name    = "firewall-public"
  network = google_compute_network.vpc_main.name

  allow {
    protocol = "icmp"  # ping
  }

  allow {
    protocol = "tcp"
    ports    = ["22", "80", "443"]
  }

  target_tags = ["firewall-public"]

  source_ranges = ["0.0.0.0/0"]
}

resource "google_compute_firewall" "firewall_private" {
  name    = "firewall-private"
  network = google_compute_network.vpc_main.name

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  target_tags = ["firewall-private"]

  source_ranges = [google_compute_subnetwork.subnet_public_main.ip_cidr_range]
}

# インスタンスの設定
resource "google_compute_instance" "instance_public_main" {
  name         = "instance-public-main"
  machine_type = "e2-micro"
  zone         = var.zone

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }

  network_interface {
    network    = google_compute_network.vpc_main.name
    subnetwork = google_compute_subnetwork.subnet_public_main.name
    access_config {
      // パブリックIPアドレスの自動割り当て
    }
  }

  metadata = {
    "block-project-ssh-keys" = "true"
    "ssh-keys"               = "ubuntu:${file(var.pub_key_path)}"
  }

  tags = ["firewall-public"]
}

resource "google_compute_instance" "instance_private_main" {
  name         = "instance-private-main"
  machine_type = "e2-micro"
  zone         = var.zone

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }

  network_interface {
    network    = google_compute_network.vpc_main.name
    subnetwork = google_compute_subnetwork.subnet_private_main.name
  }

  metadata = {
    "block-project-ssh-keys" = "true"
    "ssh-keys"               = "ubuntu:${file(var.pub_key_path)}"
  }

  tags = ["firewall-private"]
}
```

</details>

<details><summary>動作確認コマンド</summary>

```shell
# パブリックサブネット上のインスタンスに外部から通信が通ることを確認
ping <instance_public_mainのパブリックipアドレス>

# パブリックサブネット上のインスタンスから外部に通信が通ることを確認
# パブリックサブネット上のインスタンスにSSH
ssh -i ~/.ssh/id_rsa ubuntu@<instance_public_mainのパブリックipアドレス>
ping www.google.com

# プライベートサブネット上のインスタンスから外部に通信が通ることを確認
# プライベートサブネット上のインスタンスに多段SSH
ssh -o ProxyCommand='ssh -i ~/.ssh/id_rsa -W %h:%p ubuntu@<instance_public_mainのパブリックipアドレス>' -i ~/.ssh/id_rsa ubuntu@<instance_private_mainのプライベートipアドレス>
ping www.google.com
```

</details>

# GCPでのネットワーク構成
AWSではルートテーブルの設定がサブネット単位で行えるのですが、GCPではルートテーブルの設定がVPC単位でしか行えないため、AWSの構成をそのまま利用しようとしてもうまくいきません。
そこで、プライベートサブネットにCloud NATを許可することで、VMからの出口トラフィックをインターネットにルーティングします。（ただし、ルートテーブルの設定を見てもCloud NATを送信先に指定したルートが作られているわけではなく、内部でどのような実装になっているのか不思議な感じがしています、、、明示的に指定する方が安全かもしれないです）
もし厳密に行いたい場合は、インターネットアクセスを無効にしたVPCを作成して、インターネットに繋げるVPCとVPCネットワークピアリングを行うことで実現可能だと思われます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678160/042ed37a-c794-358c-fef2-c715d4170d0a.png)


構成図に現れない設定は以下の通りです
- プライベートサブネットの作成時に、限定公開のGoogleアクセスをオンにする（推奨、terraformからは設定できなそう）
- パブリックサブネットのルートテーブルの設定は不要
- 、プライベートサブネットにCloud NATを許可する。プライベートサブネットのルートテーブルの設定は不要です
- パブリックサブネットのインスタンスの外部IPの割り当てを実施、　ファイアウォール（インバウンドルール）を適時設定
- プライベートサブネットのインスタンスの外部IPの割り当てはなしとする

## 実践コード

<details><summary>backend.tf</summary>

```terraform:backend.tf
terraform {
  required_version = "~> 1.5.0"

  backend "local" {
    path = "./terraform.tfstate"
  }

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }
}
```

</details>

<details><summary>valiable.tf</summary>

```terraform:valiable.tf
variable "project" {
  type    = string
}

variable "region" {
  type    = string
  default = "asia-northeast1"
}

variable "zone" {
  type    = string
  default = "asia-northeast1-a"
}

variable "pub_key_path" {
  type    = string
  default = "~/.ssh/id_rsa.pub"
}
```

</details>

<details><summary>main.tf</summary>

```terraform:main.tf
# 利用する際は、mainをプロジェクト名に置き換えてください

provider "google" {
  project = var.project
  region  = var.region
}

# VPCとサブネットの設定
resource "google_compute_network" "vpc_main" {
  name                    = "vpc-main"
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "subnet_public_main" {
  name          = "subnet-public-main"
  ip_cidr_range = "10.0.1.0/24"
  region        = var.region
  network       = google_compute_network.vpc_main.id
}

resource "google_compute_subnetwork" "subnet_private_main" {
  name          = "subnet-private-main"
  ip_cidr_range = "10.0.2.0/24"
  region        = var.region
  network       = google_compute_network.vpc_main.id
}

# Cloud NATの設定
resource "google_compute_router" "router_main" {
  name    = "router-main"
  region  = var.region
  network = google_compute_network.vpc_main.id
}

resource "google_compute_router_nat" "nat_main" {
  name                               = "nat-main"
  router                             = google_compute_router.router_main.name
  region                             = var.region
  nat_ip_allocate_option             = "AUTO_ONLY"
  source_subnetwork_ip_ranges_to_nat = "LIST_OF_SUBNETWORKS"

  subnetwork {
    name                    = google_compute_subnetwork.subnet_private_main.name
    source_ip_ranges_to_nat = ["ALL_IP_RANGES"]
  }
}

# ファイアウォールの設定
resource "google_compute_firewall" "firewall_public" {
  name    = "firewall-public"
  network = google_compute_network.vpc_main.name

  allow {
    protocol = "icmp" # ping
  }

  allow {
    protocol = "tcp"
    ports    = ["22", "80", "443"]
  }

  target_tags = ["firewall-public"]

  source_ranges = ["0.0.0.0/0"]
}

resource "google_compute_firewall" "firewall_private" {
  name    = "firewall-private"
  network = google_compute_network.vpc_main.name

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  target_tags = ["firewall-private"]

  source_ranges = [google_compute_subnetwork.subnet_public_main.ip_cidr_range]
}

# インスタンスの設定
resource "google_compute_instance" "instance_public_main" {
  name         = "instance-public-main"
  machine_type = "e2-micro"
  zone         = var.zone

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }

  network_interface {
    network    = google_compute_network.vpc_main.name
    subnetwork = google_compute_subnetwork.subnet_public_main.name
    access_config {
      // パブリックIPアドレスの自動割り当て
    }
  }

  metadata = {
    "block-project-ssh-keys" = "true"
    "ssh-keys"               = "ubuntu:${file(var.pub_key_path)}"
  }

  tags = ["firewall-public"]
}

resource "google_compute_instance" "instance_private_main" {
  name         = "instance-private-main"
  machine_type = "e2-micro"
  zone         = var.zone

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }

  network_interface {
    network    = google_compute_network.vpc_main.name
    subnetwork = google_compute_subnetwork.subnet_private_main.name
  }

  metadata = {
    "block-project-ssh-keys" = "true"
    "ssh-keys"               = "ubuntu:${file(var.pub_key_path)}"
  }

  tags = ["firewall-private"]
}
```

</details>

<details><summary>動作確認コマンド</summary>

```shell
# パブリックサブネット上のインスタンスに外部から通信が通ることを確認
ping <instance_public_mainのパブリックipアドレス>

# パブリックサブネット上のインスタンスから外部に通信が通ることを確認
# パブリックサブネット上のインスタンスにSSH
ssh -i ~/.ssh/id_rsa ubuntu@<instance_public_mainのパブリックipアドレス>
ping www.google.com

# プライベートサブネット上のインスタンスから外部に通信が通ることを確認
# プライベートサブネット上のインスタンスに多段SSH
ssh -o ProxyCommand='ssh -i ~/.ssh/id_rsa -W %h:%p ubuntu@<instance_public_mainのパブリックipアドレス>' -i ~/.ssh/id_rsa ubuntu@<instance_private_mainのプライベートipアドレス>
ping www.google.com
```

</details>

# Azureでのネットワーク構成
Azureでは、プライベートサブネットを作成できます（cf. https://learn.microsoft.com/ja-jp/azure/virtual-network/ip-services/default-outbound-access#utilize-the-private-subnet-parameter ）ので、プライベートサブネットから外部に通信を行うためのNATゲートウェイを作成すればよいです。
ただし、terraformだと該当のオプションが見当たらなかったため、サブネットに外部からアクセスできないように設定したNSG（ネットワークセキュリティグループ）を適用することで代替しています

構成図は以下のようになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678160/6cdebdb1-8b1b-8398-bfda-72aa0f3bc0a0.png)


構成図に現れない設定は以下の通りです
- プライベートサブネットは、作成時にプライベートサブネットの設定をオンにします（推奨、terraformからは設定できなそうなので、NSGで代替）
- NATゲートウェイをプライベートサブネットに関連付けます。プライベートサブネットのルートテーブル（ユーザー定義ルート (UDR)）の設定は不要です
- パブリックサブネットのインスタンスのパブリックIPアドレスの割り当てを実施、NSGを適時設定
- プライベートサブネットのインスタンスのパブリックIPアドレスの割り当てはなしとする


## 実践コード

<details><summary>backend.tf</summary>

```terraform:backend.tf
terraform {
  required_version = "~> 1.5.0"

  backend "local" {
    path = "./terraform.tfstate"
  }

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}
```

</details>

<details><summary>valiable.tf</summary>

```terraform:valiable.tf
variable "subscription_id" {
  type    = string
}

variable "client_id" {
  type    = string
}

variable "client_secret" {
  type    = string
}

variable "tenant_id" {
  type    = string
}

variable "location" {
  type    = string
  default = "japaneast"
}

variable "public_key_path" {
  default = "~/.ssh/id_rsa.pub"
}
```

</details>

<details><summary>main.tf</summary>

```terraform:main.tf
# 利用する際は、mainをプロジェクト名に置き換えてください

provider "azurerm" {
  features {}
  subscription_id = var.subscription_id
  client_id       = var.client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id
}

resource "azurerm_resource_group" "rg_main" {
  name     = "rg_main"
  location = var.location
}

# VPCとサブネットの設定
resource "azurerm_virtual_network" "vnet_main" {
  name                = "vnet-main"
  location            = var.location
  resource_group_name = azurerm_resource_group.rg_main.name
  address_space       = ["10.0.0.0/16"]
}

resource "azurerm_subnet" "subnet_public_main" {
  name                 = "subnet-public-main"
  resource_group_name  = azurerm_resource_group.rg_main.name
  virtual_network_name = azurerm_virtual_network.vnet_main.name
  address_prefixes     = ["10.0.1.0/24"]
}

resource "azurerm_subnet" "subnet_private_main" {
  name                 = "subnet-private-main"
  resource_group_name  = azurerm_resource_group.rg_main.name
  virtual_network_name = azurerm_virtual_network.vnet_main.name
  address_prefixes     = ["10.0.2.0/24"]
}

# NAT Gatewayの設定
resource "azurerm_public_ip" "nat_public_ip" {
  name                = "nat-public-ip"
  location            = var.location
  resource_group_name = azurerm_resource_group.rg_main.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_nat_gateway" "nat_main" {
  name                = "nat-main"
  location            = var.location
  resource_group_name = azurerm_resource_group.rg_main.name
  sku_name            = "Standard"
}

resource "azurerm_nat_gateway_public_ip_association" "nat_public_ip_association_main" {
  nat_gateway_id       = azurerm_nat_gateway.nat_main.id
  public_ip_address_id = azurerm_public_ip.nat_public_ip.id
}

resource "azurerm_subnet_nat_gateway_association" "nat_subnet_association_main" {
  subnet_id      = azurerm_subnet.subnet_private_main.id
  nat_gateway_id = azurerm_nat_gateway.nat_main.id
}

# セキュリティグループの設定とサブネットへの適用
resource "azurerm_network_security_group" "nsg_public" {
  name                = "nsg-public"
  location            = var.location
  resource_group_name = azurerm_resource_group.rg_main.name

  security_rule {
    name                       = "AllowSSHHTTPHTTPS"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_ranges    = ["22", "80", "443"]
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "AllowICMP"
    priority                   = 110
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Icmp"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "AllowAllOutbound"
    priority                   = 120
    direction                  = "Outbound"
    access                     = "Allow"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

resource "azurerm_subnet_network_security_group_association" "subnet_nsg_association_public" {
  subnet_id                 = azurerm_subnet.subnet_public_main.id
  network_security_group_id = azurerm_network_security_group.nsg_public.id
}

resource "azurerm_network_security_group" "nsg_private" {
  name                = "nsg-private"
  location            = var.location
  resource_group_name = azurerm_resource_group.rg_main.name

  security_rule {
    name                       = "AllowSSH"
    priority                   = 1000
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_ranges    = ["22"]
    source_address_prefix      = azurerm_subnet.subnet_public_main.address_prefixes[0]
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "AllowAllOutbound"
    priority                   = 1000
    direction                  = "Outbound"
    access                     = "Allow"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

resource "azurerm_subnet_network_security_group_association" "subnet_nsg_association_private" {
  subnet_id                 = azurerm_subnet.subnet_private_main.id
  network_security_group_id = azurerm_network_security_group.nsg_private.id
}

# インスタンスの設定
resource "azurerm_public_ip" "vm_public_ip" {
  name                = "vm-public-ip"
  location            = var.location
  resource_group_name = azurerm_resource_group.rg_main.name
  allocation_method   = "Static"
}

resource "azurerm_network_interface" "vm_public_nic" {
  name                = "vm-public-nic"
  location            = var.location
  resource_group_name = azurerm_resource_group.rg_main.name

  ip_configuration {
    name                          = "public"
    subnet_id                     = azurerm_subnet.subnet_public_main.id
    private_ip_address_allocation = "Static"
    public_ip_address_id          = azurerm_public_ip.vm_public_ip.id
  }
}

resource "azurerm_network_interface" "vm_private_nic" {
  name                 = "vm-private-nic"
  location             = var.location
  resource_group_name  = azurerm_resource_group.rg_main.name
  enable_ip_forwarding = "true"

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.subnet_private_main.id
    private_ip_address_allocation = "Dynamic"
    # プライベートはパブリックIPが不要
  }
}

resource "azurerm_linux_virtual_machine" "vm_public_main" {
  name                = "vm-public-main"
  resource_group_name = azurerm_resource_group.rg_main.name
  location            = var.location
  size                = "Standard_B1s"
  admin_username      = "ubuntu"
  network_interface_ids = [
    azurerm_network_interface.vm_public_nic.id
  ]

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

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }

  admin_ssh_key {
    username   = "ubuntu"
    public_key = file(var.public_key_path)
  }
}

resource "azurerm_linux_virtual_machine" "vm_private_main" {
  name                = "vm-private-main"
  resource_group_name = azurerm_resource_group.rg_main.name
  location            = var.location
  size                = "Standard_B1s"
  admin_username      = "ubuntu"
  network_interface_ids = [
    azurerm_network_interface.vm_private_nic.id
  ]

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

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }

  admin_ssh_key {
    username   = "ubuntu"
    public_key = file(var.public_key_path)
  }
}
```

</details>

<details><summary>動作確認コマンド</summary>

```shell
# パブリックサブネット上のインスタンスに外部から通信が通ることを確認
ping <instance_public_mainのパブリックipアドレス>

# パブリックサブネット上のインスタンスから外部に通信が通ることを確認
# パブリックサブネット上のインスタンスにSSH
ssh -i ~/.ssh/id_rsa ubuntu@<instance_public_mainのパブリックipアドレス>
ping www.google.com

# プライベートサブネット上のインスタンスから外部に通信が通ることを確認
# プライベートサブネット上のインスタンスに多段SSH
ssh -o ProxyCommand='ssh -i ~/.ssh/id_rsa -W %h:%p ubuntu@<instance_public_mainのパブリックipアドレス>' -i ~/.ssh/id_rsa ubuntu@<instance_private_mainのプライベートipアドレス>
# AzureのNATゲートウェイではping(ICMP)はサポートされていない
# https://learn.microsoft.com/ja-jp/azure/nat-gateway/nat-overview#outbound-connectivity
curl www.google.com
```

</details>

# 参考
- https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-scenarios.html
- https://qiita.com/hesma2/items/29db7ccb8678a84d267a
- https://blog.g-gen.co.jp/entry/vpc-explained-basics
- https://learn.microsoft.com/ja-jp/azure/architecture/aws-professional/
- https://www.easydeploy.io/blog/how-to-create-azure-vnet-using-terraform/