はじめに
AWSでよく出てくる次のネットワーク構成。プライベートサブネットとパブリックサブネットを作成して、インターネットからアクセス可能なインスタンスとアクセス不可能なインスタンスを用意するやつです。
他のクラウドだと意外とこのような構成を実現する記事がなかったので、3大クラウド(AWS,GCP,Azure)で比較しながら、それぞれ実践してみました。
筆者の本業は機械学習エンジニアであり、クラウドやインフラのエンジニアではないので、この構成を利用する際はあくまで自己責任でお願いします。また、もし不備等あればご指摘いただけるとありがたいです。
作りたい構成の紹介
クラウドごとの差異もあるので、本構成として最低限必要な要素を洗い出してみます。
- インターネットからアクセス可能なネットワーク(インスタンス)が存在する
- インターネットからはアクセス不可能だが、インターネットに出ることができるネットワーク(インスタンス)が存在する
- ネットワークの分割はサブネット単位でなくても良い
※一般的にはパブリックサブネットにアプリケーションサーバー、プライベートサブネットにDBサーバーを置いたりすると思いますが、比較しやすいようにどちらもインスタンスとしています。
※ALBを利用する場合は、ALBをパブリックサブネットに配置すれば、アプリケーションサーバーもプライベートサブネットに配置できます。ただし、SSH接続を行うためには、パブリックサブネットにBastionを用意する必要があります。
AWSでのネットワーク構成
構成図は冒頭で示した通りですが、以下のようになります。
構成図に現れない設定は以下の通りです
- パブリックサブネットのルートテーブルの設定で、デフォルトゲートウェイ(0.0.0.0/0)をInternet Gatewayにする
- プライベートサブネットのルートテーブルの設定で、デフォルトゲートウェイ(0.0.0.0/0)をパブリックサブネットのNAT Gatewayにする
- パブリックサブネットのインスタンスのパブリックIPの割り当てを有効化、 ファイアウォール(インバウンドルール)を適時設定
- プライベートサブネットのインスタンスのパブリックIPの割り当てを無効化
ちなみに、AWSのネットワーク周りの設定はこちらの動画がめちゃくちゃわかりやすいので、ぜひご覧ください
実践コード
backend.tf
terraform {
required_version = "~> 1.5.0"
backend "local" {
path = "./terraform.tfstate"
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
valiable.tf
variable "region" {
type = string
default = "ap-northeast-1"
}
variable "pub_key_path" {
type = string
default = "~/.ssh/id_rsa.pub"
}
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"]
}
動作確認コマンド
# パブリックサブネット上のインスタンスに外部から通信が通ることを確認
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
GCPでのネットワーク構成
AWSではルートテーブルの設定がサブネット単位で行えるのですが、GCPではルートテーブルの設定がVPC単位でしか行えないため、AWSの構成をそのまま利用しようとしてもうまくいきません。
そこで、プライベートサブネットにCloud NATを許可することで、VMからの出口トラフィックをインターネットにルーティングします。(ただし、ルートテーブルの設定を見てもCloud NATを送信先に指定したルートが作られているわけではなく、内部でどのような実装になっているのか不思議な感じがしています、、、明示的に指定する方が安全かもしれないです)
もし厳密に行いたい場合は、インターネットアクセスを無効にしたVPCを作成して、インターネットに繋げるVPCとVPCネットワークピアリングを行うことで実現可能だと思われます。
構成図に現れない設定は以下の通りです
- プライベートサブネットの作成時に、限定公開のGoogleアクセスをオンにする(推奨、terraformからは設定できなそう)
- パブリックサブネットのルートテーブルの設定は不要
- 、プライベートサブネットにCloud NATを許可する。プライベートサブネットのルートテーブルの設定は不要です
- パブリックサブネットのインスタンスの外部IPの割り当てを実施、 ファイアウォール(インバウンドルール)を適時設定
- プライベートサブネットのインスタンスの外部IPの割り当てはなしとする
実践コード
backend.tf
terraform {
required_version = "~> 1.5.0"
backend "local" {
path = "./terraform.tfstate"
}
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
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"
}
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"]
}
動作確認コマンド
# パブリックサブネット上のインスタンスに外部から通信が通ることを確認
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
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(ネットワークセキュリティグループ)を適用することで代替しています
構成図に現れない設定は以下の通りです
- プライベートサブネットは、作成時にプライベートサブネットの設定をオンにします(推奨、terraformからは設定できなそうなので、NSGで代替)
- NATゲートウェイをプライベートサブネットに関連付けます。プライベートサブネットのルートテーブル(ユーザー定義ルート (UDR))の設定は不要です
- パブリックサブネットのインスタンスのパブリックIPアドレスの割り当てを実施、NSGを適時設定
- プライベートサブネットのインスタンスのパブリックIPアドレスの割り当てはなしとする
実践コード
backend.tf
terraform {
required_version = "~> 1.5.0"
backend "local" {
path = "./terraform.tfstate"
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
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"
}
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)
}
}
動作確認コマンド
# パブリックサブネット上のインスタンスに外部から通信が通ることを確認
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
参考
- 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/