前回の記事を書いた段階から構想としてあった、AzureとAWS間でのインターネットVPN、しかもTerraformで完全コード化したものを公開します。
GUIだと楽勝なんで簡単に考えていたんですが、コード化するとなると中々手強かったです。
お盆前には公開できる準備は整っていたのですが、子どもの夏休みの宿題と自身の体調不良で時間が空いてしまいました。
体調不良は置いといて、夏休みの宿題、面白いですね。
大人になってから見てみるととても感慨深いです。
今は読書感想文のほかに、映画鑑賞による感想文があり、うちの子はこの映画鑑賞の感想文を選んでいました。
私も小学校5年生あたりで夏休みの宿題とは別で新聞社がやってる作文コンクールに応募して佳作だったか努力賞だったかの賞をもらったことがあります。
父に何度も校正されて、子どもの頃の生意気な私なら途中で投げ出していたと思うのですが、なぜかとても楽しく、書きたいことがいっぱいあるタイプの人間なので文字数制限内に収めるためいろいろな表現を学ぶ良い経験になったので、子どもにも勧めました。
いや、作文コンクール、読書感想文、映画の感想文、よく考えられた仕組みですよね。
文字数制限ってなんであるの?って子供心ながら思ってたんですが、やっぱり要約する力や語彙力を高めるためなんですかね?
宿題を見る側になって改めて気付きました。
元々日本には俳句みたいに字数制限のある文化が根付いているのもあり、ある意味縛りプレイが好きなのかもしれませんね。
子どもの感想文も良くかけていて、私と同じタイプなのか文字数制限に収めるため四苦八苦していました。
「書きたいことを如何に絞るか、如何に少ない字数で読み手に映画の面白さを伝えるか」が目的であると子どもに伝えつつ、この目的を達成するために要約する力と語彙力を学ぶんだよ、と教えました。
日本語の様々な表現に触れる良い機会になったかなと思います。
他にも書き方として演繹法や帰納法というワードも含めて子どもに伝えていますが、まだそこまで意識する世代でもないかなというのと、あまり情報を与えすぎで子どもの表現する自由を制限するのも良くないかなと思い私自身も子どもに伝える情報を取捨選択しました。
字数制限についてのメリットは理解しているつもりですが、あくまでつもり
なので、私はQiitaでは字数なんて気にせず言いたいことを言いたいだけ書いちゃいます。
ダメな親だ・・・
構成図
はい。
今回も与太話が長くなってしまいましたが構成図です。
今回はちょっと横長になっちゃいましたが、こんな感じです。
Azure、AWS、双方NAT Gatewayを作っても良かったんですが、今回も例に漏れず自腹なので節約の意味からもNAT Gatewayは作りませんでした。
お金以外にもNAT Gatewayの有無は本件の意図するところとそこまで大きな影響は無いので省略でも問題ないですね。
構成をいたずらに複雑にする意味もありませんし。
今回こだわったところ
本記事の構成は冒頭でも書きましたがGUIだと楽勝です。
今回こだわった部分は、Azure、AWS、2つの環境を別々でコード実行して最終的にVPNを張るのではなく、1回のコード実行でVPNを張る部分までノンストップで完遂することです。
GUIではどうしてもAzureかAWSかで先に環境を作成し、その後他方のクラウドキャリアで先に作成した環境に合わせて構築を行うという順序にならざるを得ませんが、これが面倒なので1回のコード実行、具体的にはterraform apply
でAzure、AWS双方の環境を構築することにこだわりました。
今回難しかったところ
コードの紹介しようかなと思ったんですが、演繹法的に本論前半で難しかった部分を記載します。
AzureとAWS両方のProvider情報を併記する
今回こだわった部分は、先にも記載の通りAzure、AWS、2つの環境を別々でコード実行して最終的にVPNを張るのではなく、1回のコード実行でVPNを張る部分までノンストップで完遂することです。
なのでProvider情報をAzure、AWSそれぞれで分けて記載していた部分、具体的にはmain.tf
を1つにまとめなければなりませんでした。
これが難しかったです。
AzureとAWS、両方で同じ変数名を使っていたので、これをそれぞれ一意の変数名に再命名しなければならない
これも1回のコード実行でAzure-AWS間のVPN接続まで完遂することにこだわると当たり前なのですが、まぁまぁめんどくさかったです。
Resource GroupみたいにAzureにしか存在しない設定値(≒変数)や、NSG、Security Groupのように同じような役割でもリソース名が違うので変数名が分かれているものは楽なんですが、サブネット名、サブネットのIPアドレス帯Suffix(第3オクテットと第4オクテット)、リージョンのようにAzure、AWSに共通する考え方、かつ必須の設定は運悪く、というかある意味当然なのですが変数名が重複していました。
これを再命名し、関連するコード部分をすべて変更するのが手間でした。
AWSのVPN接続リソース(AWS VPN Connection)で出力されるAWSのVPNゲートウェイのパブリックIPアドレスを変数として受け取り、AzureのLocal GatewayのパブリックIPアドレスとして処理する
本記事ではこれに一番時間がかかりました。
Terraform公式にも直接的に言及している記載はなく、Try&Errorを繰り返しました。
AzureのVPN Gatewayが構築に最大30分かかるという制約があることから、このTry&Errorにも時間がかかり、かつお金もかかりました。
実際に検証していた際30分はかかりませんでしたが、それでも15分程度は毎回時間がかかるので、Azure VPN Gateway構築の待ち時間にAWS側の課金がかかるのがちょと嫌でした。
さすがにこのTry&Error部分ではAzure VMやAWS EC2の構築は無視して、Azure Virtual Gateway、AWS 仮想プライベートゲートウェイ、Azure Local Gateway、AWS Customer Gatewayの作成のみに絞って検証を行いました。
Terraform
ではこだわりポイントや難しかった部分も説明できたので、コードのご紹介です。
AzureとAWS両方のコードを記載していきますが、変数設定やmain.tfのような共通項目以外はそれぞれのコードを記載したファイルにどちらのクラウドキャリアの設定なのかプレフィックスを付けました。
いつもなるべく環境の外側からコードを記載しているので、今回もこの順で、かつアルファベット順でコードを紹介します。
変数設定
# ---------------------------
# AWS Variables - 変数設定
# ---------------------------
# region
# ap-northeast-1 東京リージョン
# ap-south-1 ムンバイリージョン
variable "aws_region" {
default = "ap-south-1"
}
# 環境種別(本番:prd,ステージング:stg,開発:dev)
variable "env_type" {
default = "dev"
}
# システム名
variable "sys_name" {
default = "aws-azure-vpn01"
}
# availability_zone
variable "availability_zone" {
type = object({
a = string
b = string #ムンバイリージョンで利用
c = string
})
default = {
a = "ap-south-1a" # ムンバイ(インド)のアベイラビリティゾーン
b = "ap-south-1b" # ムンバイ(インド)のアベイラビリティゾーン
c = "ap-south-1c" # ムンバイ(インド)のアベイラビリティゾーン
}
}
# ------------------------------
# VPC 関連
# ------------------------------
# vpc address
variable "vpc_address_pre" {
default = "10.0."
}
variable "vpc_address_suf" {
default = "0.0/23"
}
# private subnet suffix address01
variable "private_sn_address_suf01" {
default = "1.0/24"
}
# public subnet suffix address01
variable "public_sn_address_suf01" {
default = "0.0/24"
}
# VPC Endpoint
variable "vpc_endpoints" {
type = list(any)
default = ["ssm", "ssmmessages", "ec2messages"]
}
# ------------------------------
# EC2 作成
# ------------------------------
# Linux EC2のインスタンスタイプ
variable "ec2_instance_type_lin" {
default = "t2.micro"
}
# Windows EC2のインスタンスタイプ
variable "ec2_instance_type_win" {
default = "m4.large"
}
# ---------------------------
# Azure Variables - 変数設定
# ---------------------------
# Import Subscription ID variable from OS's Enviroment variable
variable "ARM_SUBSCRIPTION_ID" {
type = string
}
# Import Tenant ID variable from OS's Enviroment variable
variable "ARM_TENANT_ID" {
type = string
}
# Import Client ID variable from OS's Enviroment variable
variable "ARM_CLIENT_ID" {
type = string
}
# Import Client Secret variable from OS's Enviroment variable
variable "ARM_CLIENT_SECRET" {
type = string
}
# Generate random text for a unique storage account name
resource "random_id" "random_id" {
keepers = {
# Generate a new ID only when a new resource group is defined
resource_group = azurerm_resource_group.rg.name
}
byte_length = 8
}
# system name
variable "system_name" {
type = string
default = "azure-aws-vpn01"
}
# region
variable "azure_region" {
type = string
default = "southeastasia"
}
# resource group (rg) name prefix
variable "rg_name_pre" {
type = string
default = "rg-"
}
# virtual network (vnet) name prefix
variable "vnet_name_pre" {
type = string
default = "vnet-"
}
# virtual network (vnet) name suffix for client
variable "vnet_name_suf01" {
type = string
default = "azure-aws-vpn01"
}
# vnet ip address prefix01 for client
variable "vnet_addr_pre01" {
type = string
default = "192.168."
}
# vnet ip address suffix for client
variable "vnet_addr_suf01" {
type = string
default = "200.0/23"
}
# subnet (sn) name prefix
variable "sn_name_pre" {
type = string
default = "sn-"
}
# sn for client name suffix for client
variable "sn_name_suf01" {
type = string
default = "client01"
}
# sn for clinet ip address suffix for client
variable "sn_addr_suf01" {
type = string
default = "201.0/24"
}
# sn for clinet ip address suffix for Virtual Network Gateway
variable "sn_addr_suf02" {
type = string
default = "200.0/24"
}
# Network Interface Card (nic) name prefix
variable "nic_name_pre" {
type = string
default = "nic-"
}
# Network Security Group (nsg) name prefix
variable "nsg_name_pre" {
type = string
default = "nsg-"
}
# Public IP Address (pip) prefix
variable "pip_name_pre" {
type = string
default = "pip-"
}
# IP Configuration (ipconf) suffix
variable "ipconf_suf" {
type = string
default = "-configuration"
}
# Private IP Address Allocation (priv_ip_alloc)
variable "priv_ip_alloc" {
type = string
default = "Dynamic" # Dynamic or Static
}
# admin user (adminuser) name
variable "adminuser" {
type = string
default = "init-Admin001"
}
# Virtual Machine OS Disk (osdisk) name prefix
variable "vm_osdisk_pre" {
type = string
default = "osdisk-"
}
# Virtual Machine OS Disk SKU (osdisk_sku)
variable "vm_osdisk_sku" {
type = string
default = "StandardSSD_LRS"
}
# Virtual Machine SKU (vmsku) name
variable "vm_sku" {
type = string
default = "Standard_D4_v5"
}
# Virtual Machine (vm) name prefix
variable "vm_name_pre" {
type = string
default = "vm-"
}
# vm name suffix01
variable "vm_name_suf01" {
type = string
default = "win11-01"
}
# vm name suffix02
variable "vm_name_suf02" {
type = string
default = "alma-01"
}
# Virtual Machine OS Publisher (os_pub) name
# "MicrosoftWindowsDesktop" is Client OS
# "MicrosoftWindowsServer" is Server OS
# "Canonical" is Ubuntu
# "almalinux" is Almalinux
variable "os_pub01" {
type = string
default = "MicrosoftWindowsDesktop"
}
variable "os_pub02" {
type = string
default = "almalinux"
}
# Virtual Machine OS Offer (os_offer) name
# "Windows-10" is Windows 10
# "Windows-11" is Windows 11
# "WindowsServer" os Windows Server OS
# "0001-com-ubuntu-server-jammy" is Ubuntu
# "almalinux-x86_64" is Almalinux
variable "os_offer01" {
type = string
default = "Windows-11"
}
variable "os_offer02" {
type = string
default = "almalinux-x86_64"
}
# Virtual Machine OS (os_sku) name
# 2016-Datacenter
# 2019-Datacenter
# 2022-datacenter-azure-edition
# 22_04-lts-gen2 (Ubuntu 22.04 LTS)
# 9-gen2 (Almalinux 9)
variable "os_sku01" {
type = string
default = "win11-22h2-pro"
}
variable "os_sku02" {
type = string
default = "9-gen2"
}
# Virtual Machine OS Version (os_ver) name
variable "os_ver" {
type = string
default = "latest"
}
# Local Gateway name prefix
variable "LG_name_pre" {
type = string
default = "lg-"
}
# Local Gateway name suffix
variable "LG_name_suf" {
type = string
default = "aws01"
}
# VPN connection name prefix
variable "connection_name_pre" {
type = string
default = "con-"
}
努力の跡は見ていただけたでしょうか?
今まで設定していた変数名を変更しています。
変数設定の項目はAzureの方がぶっちぎりで多いですね。
私自身がAzureの方がAWSより詳しいので、Azureの方をより詳細に設定できる、というのが大きな要因の気もしますが(ようは私がAWSにあまり詳しくない)これが実際に作成されるリソース数と比較したらどうなるかも動作確認の項目で確認しましょう。
main.tf
# ---------------------------
# main
# ---------------------------
terraform {
required_version = ">= 1.4" # Terraformのバージョンを1.4以上に指定
required_providers {
# AWSのProvider情報
aws = {
source = "hashicorp/aws"
version = "= 5.22.0" # AWSプロバイダのバージョンを5.22.0に固定
}
# AzureのProvider情報
azapi = {
source = "azure/azapi"
version = "~>1.5"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
random = {
source = "hashicorp/random"
version = "~>3.0"
}
}
}
# プロバイダー設定
provider "aws" {
region = var.aws_region
}
provider "azapi" {
}
provider "azurerm" {
features {}
subscription_id = "${var.ARM_SUBSCRIPTION_ID}"
tenant_id = "${var.ARM_TENANT_ID}"
client_id = "${var.ARM_CLIENT_ID}"
client_secret = "${var.ARM_CLIENT_SECRET}"
}
はい。
ここでも努力の跡は見ていただけたでしょうか?
今回難しかったところでも挙げていますが、このmain.tfがそもそもこの記述方法でちゃんと動くのか全然わからないところからのスタートでした。
ちゃんと動いたので良かったですが、動かなかったらその時点で本記事は詰みなんですよね。
aws_iam.tf
# ---------------------------
# IAM
# ---------------------------
# IAM ROLE 定義
resource "aws_iam_role" "ec2_role" {
name = "${var.env_type}-${var.sys_name}-iam-role"
assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
}
# IAMポリシー定義
data "aws_iam_policy_document" "ec2_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
# IAM ROLEのインスタンスプロフィールの作成
resource "aws_iam_instance_profile" "instance_prof" {
name = "${var.env_type}-${var.sys_name}-instance-profile-ssm"
role = aws_iam_role.ec2_role.name
}
# ---------------------------
# FOR SSM
# ---------------------------
# SSM用ポリシーをEC2ロールに設定
resource "aws_iam_role_policy_attachment" "ssm_control" {
role = aws_iam_role.ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
はい。
安定のaws_iam.tf
ですね。
SSMを実装します。
aws_vpc.tf
# ---------------------------
# VPC 構築
# ---------------------------
# VPC
resource "aws_vpc" "vpc" {
cidr_block = "${var.vpc_address_pre}${var.vpc_address_suf}"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.env_type}-${var.sys_name}-vpc"
}
}
# ---------------------------
# Public Subnet 構築
# ---------------------------
resource "aws_subnet" "sn_public_1a" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.vpc_address_pre}${var.public_sn_address_suf01}"
availability_zone = var.availability_zone.a
tags = {
Name = "${var.env_type}-${var.sys_name}-sn-public-1a"
}
}
# ---------------------------
# Private Subnet 構築
# ---------------------------
resource "aws_subnet" "sn_private_1b" {
vpc_id = aws_vpc.vpc.id
cidr_block = "${var.vpc_address_pre}${var.private_sn_address_suf01}"
availability_zone = var.availability_zone.b
tags = {
Name = "${var.env_type}-${var.sys_name}-sn-private-1b"
}
}
# ---------------------------
# Internet Gateway 作成
# ---------------------------
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.env_type}-${var.sys_name}-igw"
}
}
# ---------------------------
# Route table 作成
# ---------------------------
# Public Subnet 用Route Table 作成
resource "aws_route_table" "rt_sn_public_1a" {
vpc_id = aws_vpc.vpc.id
# Default GatewayをInternet Gatewayに向ける
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = {
Name = "${var.env_type}-${var.sys_name}-rt-sn-public-1a"
}
}
# Private Subnet 用Route Table 作成
resource "aws_route_table" "rt_sn_private_1b" {
vpc_id = aws_vpc.vpc.id
# Default GatewayをInternet Gatewayに向ける
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
route {
cidr_block = "${var.vnet_addr_pre01}${var.vnet_addr_suf01}"
gateway_id = aws_vpn_gateway.vgw.id
}
tags = {
Name = "${var.env_type}-${var.sys_name}-rt-sn-private-1b"
}
}
# Public Subnet とDefault Route table の関連付け
resource "aws_route_table_association" "associate_rt_sn_public_1a_sn_public_1a" {
subnet_id = aws_subnet.sn_public_1a.id
route_table_id = aws_route_table.rt_sn_public_1a.id
}
# Private Subnet とDefault Route table の関連付け
resource "aws_route_table_association" "associate_rt_sn_private_1b_sn_private_1b" {
subnet_id = aws_subnet.sn_private_1b.id
route_table_id = aws_route_table.rt_sn_private_1b.id
}
VPCですね。
大した設定はしていません。
aws_vpc_endpoint.tf
# ---------------------------
# VPC Endpoint
# ---------------------------
# SSM用 VPC endpointの作成
resource "aws_vpc_endpoint" "interface" {
for_each = toset(var.vpc_endpoints)
vpc_id = aws_vpc.vpc.id
service_name = "com.amazonaws.${var.aws_region}.${each.value}"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.sn_public_1a.id]
private_dns_enabled = true
security_group_ids = [aws_security_group.sg_ssm.id]
tags = { "Name" = "${var.env_type}-${var.sys_name}-vpc-endpoint-${each.value}" }
}
はい。
こちらもいつもの安定のvpc_endpoint.tf
ですね。
SSMに必須ですね。
Linux OSにはsshで、Windows OSにはfleet managerで接続できる方法を確保します。
動作確認もこのSSMで行います。
aws_security_group.tf
# ---------------------------
# Security Group
# ---------------------------
# ---------------------------
# EC2用 Security Group作成
# ---------------------------
#Linux EC2 用SG
resource "aws_security_group" "sg_ec2_linux01" {
name = "${var.env_type}-${var.sys_name}-sg-ec2-linux01"
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.env_type}-${var.sys_name}-sg-ec2-linux01"
}
# インバウンドルール
# from SSM
ingress {
description = "AllowHttpsInBoundFromVPC for SSM"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["${var.vpc_address_pre}${var.vpc_address_suf}"]
}
ingress {
description = "AllowICMPv4InBoundFromVPC"
from_port = -1
to_port = -1
protocol = "icmp"
cidr_blocks = ["${var.vpc_address_pre}${var.vpc_address_suf}"]
}
# from Azure Vnetアクセス
ingress {
description = "AllowInBoundFrom_Azure-Vnet01"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["${var.vnet_addr_pre01}${var.vnet_addr_suf01}"]
}
# アウトバウンドルール
egress {
description = "AllowAnyOutBound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Windows Server 用SG
resource "aws_security_group" "sg_ec2_windows01" {
name = "${var.env_type}-${var.sys_name}-sg-ec2-windows01"
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.env_type}-${var.sys_name}-sg-ec2-windows01"
}
# インバウンドルール
# from VPC ICMPv4
ingress {
description = "AllowICMPv4InBoundFromVPC"
from_port = -1
to_port = -1
protocol = "icmp"
cidr_blocks = ["${var.vpc_address_pre}${var.vpc_address_suf}"]
}
# from Azure Vnetアクセス
ingress {
description = "AllowInBoundFrom_Azure-Vnet01"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["${var.vnet_addr_pre01}${var.vnet_addr_suf01}"]
}
# アウトバウンドルール
egress {
description = "AllowAnyOutBound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# ---------------------------
# SSM用 Vpc endpoint Security Group作成
# ---------------------------
resource "aws_security_group" "sg_ssm" {
name = "${var.env_type}-${var.sys_name}-sg-ssm"
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.env_type}-${var.sys_name}-sg-ssm"
}
# インバウンドルール
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["${var.vpc_address_pre}${var.vpc_address_suf}"]
}
# アウトバウンドルール
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "sg_ssmmessages" {
name = "${var.env_type}-${var.sys_name}-sg-ssmmessages"
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.env_type}-${var.sys_name}-sg-ssmmessages"
}
# インバウンドルール
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["${var.vpc_address_pre}${var.vpc_address_suf}"]
}
# アウトバウンドルール
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "sg_ec2messages" {
name = "${var.env_type}-${var.sys_name}-sg-ec2messages"
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.env_type}-${var.sys_name}-sg-ec2messages"
}
# インバウンドルール
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["${var.vpc_address_pre}${var.vpc_address_suf}"]
}
# アウトバウンドルール
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
はい。
SSM許可してます。
特筆すべきはAzure Virtual Networkからの接続を許可しているくらいですかね。
aws_vpn_gateway.tf
# ---------------------------
# AWS VPN Gateway 作成
# ---------------------------
resource "aws_vpn_gateway" "vgw" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.env_type}-${var.sys_name}-vgw"
}
}
ここではAWS_VPN_GatewayとTerraformのコードから命名していますが、AWSの日本語のGUIの表現だと仮想プライベートゲートウェイです。
コード自身はめっちゃ短いですね。
AzureとAWSの違いからこのような設定数の差があるのですが、VPNを張る際AWSはVPN Connectionの項目でVPN関連の多くを設定します。
AzureはVPN Gatewayの項目でVPN関連の多くを設定します。
理由はわかりませんが、こうなっています。
クラウドキャリアの設計思想の違いなのでしょうが、VPN Gatewayはオンプレミスで言うところのルーター、もうちょっと言及するとボーダールーターにあたるので、ルーター関連の設定はルーターに相当するリソースの設定値に寄せてほしいな、というのが私個人の意見です。
aws_customer_gateway.tf
# ---------------------------
# Customer Gateway 作成 (実質対抗のAzure 仮想ネットワーク ゲートウェイ)
# ---------------------------
resource "aws_customer_gateway" "cgw" {
bgp_asn = 65000 # default
ip_address = "${data.azurerm_public_ip.pip-vgw.ip_address}"
type = "ipsec.1"
tags = {
Name = "${var.env_type}-${var.sys_name}-cgw"
}
depends_on = [
azurerm_virtual_network_gateway.vgw01
]
}
はい。
ここですね!難しかったポイントは!
ip_address = "${data.azurerm_public_ip.pip-vgw.ip_address}"
ここです!
お分かりいただけますでしょうか?
わざわざ、data
という変数を作って、ここにAzureのVPN GatewayのパブリックIPアドレスの構築後に出力されるIPアドレスの実値を格納しなければなりませんでした。
直接的にこの部分に言及されている情報はないのですが、間接的に言及されている部分がTerraformの公式にありました。
こちらです。
https://github.com/hashicorp/terraform-provider-azurerm/issues/159
ここにissueとして挙げられているのはAzure VMですが、Azure VPN Gatewayも挙動としては同じです。
わざわざこんな変数(data
)を作らないといけないのもAzure VMとAzure VPN Gateway共通の理由で、AzureのPublic IP Addressの仕様として、DynamicでIP Addressを指定した場合は、Public IP Addressが関連付く対象リソースのデプロイが完了してからでないとIP Addressが割り当てられないというのがその理由です。
これは私も思い当たる節があり、今までAzure VPN GatewayやAzure VM、他のPublic IP Addressを必要とする各種リソースの構築時に毎度この挙動はGUIでも確認していました。
先のissue報告内容は
「Azure VMに割り当てたPublic IP Addressはすぐに出力することはできず、構築完了後にterraform refresh
というコマンドを実行してから再取得しなければならない。」
となっています。
確かにその通りなのですが、これもまた先ほどのissue報告の返信に記載されている通りこの挙動はAzureの仕様を受けたTerraformの仕様です。
今回のこだわりポイントはあくまで1回のコード実行でAzure-AWS間のVPNを張る部分までノンストップで完遂することです。
ですのでこのissueにある通りterraform refresh
を実行し、そこでAzure VPN GatewayのPublic IP Addressを取得し、そこから再度terraform apply
を実行する、ではこだわりを満たせません。
ですので、いったん出力される予定のPublic IP Addressを変数data
に入れ、これをAWS Customer Gatewayのパラメーターとして指定します。
具体的な変数data
の作成の仕方はAzure VPN Gatewayで設定していますのでもう少々お待ちください。
aws_ec2.tf
# -------------------------------
# EC2作成
# -------------------------------
# -------------------------------
# Amazon Linux EC2を作成
# -------------------------------
# Amazon Linux 2023 の最新版AMIを取得
data "aws_ssm_parameter" "al2023-ami-kernel-default-x86_64" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}
resource "aws_spot_instance_request" "ec2_linux01" { #spot_priceを設定する。ムンバイリージョンではAvailability Zone CではSpot Instanceは使えない。
spot_price = "0.0029"
ami = data.aws_ssm_parameter.al2023-ami-kernel-default-x86_64.value
instance_type = var.ec2_instance_type_lin
availability_zone = var.availability_zone.b
vpc_security_group_ids = [aws_security_group.sg_ec2_linux01.id]
subnet_id = aws_subnet.sn_private_1b.id
associate_public_ip_address = "false"
iam_instance_profile = aws_iam_instance_profile.instance_prof.name
user_data = <<-EOF
#!/bin/bash
sudo hostnamectl set-hostname ${var.env_type}-${var.sys_name}-ec2-linux01
EOF
root_block_device {
volume_size = 30 # GB
volume_type = "gp3" # 汎用SSD
encrypted = false
tags = {
Snapshot = "false"
}
}
tags = {
Name = "${var.env_type}-${var.sys_name}-ec2-amazon-linux01"
}
depends_on = [
aws_route_table_association.associate_rt_sn_private_1b_sn_private_1b
]
}
# -------------------------------
# Windows Server EC2を作成
# -------------------------------
# -------------------------------
# Windows Server 用のKey pair作成
# -------------------------------
variable "key_name" {
default = "kp-ec2-01"
}
variable "key_path" {
# - Windowsの場合はフォルダを"\\"で区切る(エスケープする必要がある)実行中のtfファイルと同じ場所にキーファイルを出力する
default = ".\\"
}
resource "tls_private_key" "kp-windows-ec2-01" {
algorithm = "RSA"
rsa_bits = 2048
}
# クライアントPCにKey pair(秘密鍵と公開鍵)を作成
# - Windowsの場合はフォルダを"\\"で区切る(エスケープする必要がある)
# - [terraform apply] 実行後はクライアントPCの公開鍵は自動削除される
locals {
public_key_file = "${var.key_path}\\${var.key_name}.id_rsa.pub"
private_key_file = "${var.key_path}\\${var.key_name}.id_rsa"
}
resource "local_file" "kp_windows_ec2_01_pem" {
filename = "${local.private_key_file}"
content = "${tls_private_key.kp-windows-ec2-01.private_key_pem}"
}
# 上記で作成した公開鍵をAWSのKey pairにインポート
resource "aws_key_pair" "kp_windows_ec2_01" {
key_name = "${var.key_name}"
public_key = "${tls_private_key.kp-windows-ec2-01.public_key_openssh}"
}
#20240523時点のAMI
#Microsoft Windows Server 2022 Base:ami-0f346136f3b372267
#Microsoft Windows Server 2019 Base:ami-01bd28d73d0053a15
#Microsoft Windows Server 2016 Base:ami-063d3d00f8a97a6d1
resource "aws_spot_instance_request" "ec2_windows01" { #spot_priceを設定する。ムンバイリージョンではAvailability Zone CではSpot Instanceは使えない。
spot_price = "0.1161"
ami = "ami-0f346136f3b372267" #Microsoft Windows Server 2022 Base
instance_type = var.ec2_instance_type_win
availability_zone = var.availability_zone.b
vpc_security_group_ids = [aws_security_group.sg_ec2_windows01.id]
subnet_id = aws_subnet.sn_private_1b.id
associate_public_ip_address = "false"
key_name = "${var.key_name}"
iam_instance_profile = aws_iam_instance_profile.instance_prof.name
root_block_device {
volume_size = 50 # GB
volume_type = "gp3" # 汎用SSD
encrypted = false
tags = {
Snapshot = "false"
}
}
tags = {
Name = "${var.env_type}-${var.sys_name}-ec2-windows01"
}
depends_on = [
aws_route_table_association.associate_rt_sn_private_1b_sn_private_1b
]
}
はい。
EC2です。
今回もSpot Instanceです!
お金は大事ですからね!
つづいてAzureです。
azure_resource_group.tf
# --------------------------------------
# Create Resource Group
# --------------------------------------
resource "azurerm_resource_group" "rg" {
location = var.azure_region
name = "${var.rg_name_pre}${var.system_name}"
}
はい。
安定のAzure Resource Groupですね。
何も言うことはありません。
azure_vnet.tf
#----------------------------------------
# client vnet Create
#----------------------------------------
resource "azurerm_virtual_network" "vnet-client" {
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
name = "${var.vnet_name_pre}${var.vnet_name_suf01}"
address_space = [ "${var.vnet_addr_pre01}${var.vnet_addr_suf01}" ]
}
# sn-client01 Create
resource "azurerm_subnet" "sn-client01" {
resource_group_name = azurerm_resource_group.rg.name
name = "${var.sn_name_pre}${var.sn_name_suf01}"
virtual_network_name = azurerm_virtual_network.vnet-client.name
address_prefixes = [ "${var.vnet_addr_pre01}${var.sn_addr_suf01}" ]
service_endpoints = [ "Microsoft.Storage" ]
}
はい。
これも特に言及するところはありません。
azure_vnet_gateway.tf
# ------------------------------------------
# Create Virtual Network Gateway Subnet
# ------------------------------------------
resource "azurerm_subnet" "GatewaySubnet" {
resource_group_name = azurerm_resource_group.rg.name
name = "GatewaySubnet"
virtual_network_name = azurerm_virtual_network.vnet-client.name
address_prefixes = [ "${var.vnet_addr_pre01}${var.sn_addr_suf02}" ]
}
# ------------------------------------------
# Create Virtual Network Gateway
# ------------------------------------------
resource "azurerm_public_ip" "pip-vgw" {
name = "pip-vgw01"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Dynamic"
}
resource "azurerm_virtual_network_gateway" "vgw01" {
name = "vgw01"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
type = "Vpn"
vpn_type = "RouteBased"
active_active = false
enable_bgp = false
sku = "Basic"
ip_configuration {
name = "vnetGatewayConfig"
public_ip_address_id = azurerm_public_ip.pip-vgw.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.GatewaySubnet.id
}
}
# VGWのPublic IP Addressを取得するためのコード
data "azurerm_public_ip" "pip-vgw" {
name = "${azurerm_public_ip.pip-vgw.name}"
resource_group_name = azurerm_resource_group.rg.name
depends_on = [
azurerm_virtual_network_gateway.vgw01
]
}
はい。
ここで出てきますね。
本記事の前述部分で言及しているdata
です。
最後のブロックですね。
ここでdepends_on
を使って構成順序を調整しており、先のAzureの仕様、「Public IP Addressが付与されているリソースの構築が完了しないとPublic IP Addressが割り当てられない」を回避しています。
ここに行きつくまで多分4回から5回くらい再構築しましたね・・・
azure_local_gateway.tf
# --------------------------------------
# Create Azure Local Network Gateway
# --------------------------------------
resource "azurerm_local_network_gateway" "lgw" {
name = "${var.LG_name_pre}${var.LG_name_suf}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
gateway_address = "${aws_vpn_connection.vpn_connection.tunnel1_address}"
address_space = ["${var.vpc_address_pre}${var.vpc_address_suf}"]
}
こちらもAWSのCustomer GatewayからAzure Local GatewayのPublic IP Addressを参照しています。
でもAWS Customer GatewayはAzure VPN Gatewayのようにややこしい仕様では無いので単純な指定、
"${aws_vpn_connection.vpn_connection.tunnel1_address}"
で済んでいます。
シンプルで良いですね。
azure_nsg.tf
#------------------------------------
# nsg-sn-client01 Create
#------------------------------------
resource "azurerm_network_security_group" "nsg-sn-client01" {
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
name = "${var.nsg_name_pre}${azurerm_subnet.sn-client01.name}"
## InBound Rule
security_rule {
name = "AllowVnetInBound"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "VirtualNetwork"
destination_address_prefix = "VirtualNetwork"
}
security_rule {
name = "AllowAzureLoadBalancerInBound"
priority = 101
direction = "Inbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "AzureLoadBalancer"
destination_address_prefix = "*"
}
security_rule {
name = "DenyAllInBound"
priority = 4096
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
## OutBound Rule
security_rule {
name = "AllowVnetOutBound"
priority = 100
direction = "Outbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "VirtualNetwork"
destination_address_prefix = "VirtualNetwork"
}
security_rule {
name = "AllowAzureFrontDoor.FirstPartyHttpOutBound"
priority = 101
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "AzureFrontDoor.FirstParty"
description = "Allow Windows Update Rule01"
}
security_rule {
name = "AllowAzureUpdateDeliveryHttpsOutBound"
priority = 102
direction = "Outbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "AzureUpdateDelivery"
description = "Allow Windows Update Rule02"
}
security_rule {
name = "AllowAllOutBound"
priority = 4095
direction = "Outbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
security_rule {
name = "DenyAllOutBound"
priority = 4096
direction = "Outbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
# Connect nsg-sn-client01 to sn-client01
resource "azurerm_subnet_network_security_group_association" "nsg-sn-client01-to-sn-client01" {
subnet_id = azurerm_subnet.sn-client01.id
network_security_group_id = azurerm_network_security_group.nsg-sn-client01.id
depends_on = [
azurerm_virtual_network_gateway_connection.vpn-connection
]
}
はい。
ここもAWSのSecurity Groupでも言及していますが、AzureのNSGではVPNの対向先、今回だとAWSのVPCで指定されているローカルIPアドレスセグメントを許可する必要はありません。
これはAzureの仕様です。
もう少し細かく言及すると、Azureの仕様上、VPNの対向先で指定されるローカルIPアドレスセグメント、設定値としてはAzure Local Gatewayで指定されているローカルIPアドレスセグメントは暗黙的にNSGのタグのVirtual Networkに含まれるからです。
これも豆知識でAzure構築の際にひっかかるあるあるなので覚えておいて損はないでしょう。
そして私はNSGのコード内で最上位の優先順位100でInBoundもOutBoundもVirtual Network to Virtual NetworkはAny Any Permitしてあるので、このVPN間の通信は最初に許可されます。
azure_storage.tf
# Create storage account for boot diagnostics
resource "azurerm_storage_account" "storage_account" {
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
name = "stdiag${random_id.random_id.hex}"
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "azurerm_storage_account_network_rules" "for_multiple_subnet_id" {
storage_account_id = azurerm_storage_account.storage_account.id
default_action = "Deny"
virtual_network_subnet_ids = [azurerm_subnet.sn-client01.id]
ip_rules = ["123.225.10.132"]
}
はい。
ここは特に何かっていうことはないですね。
azure_vm.tf
# --------------------------------------
# Create Windows 11
# --------------------------------------
# nic01 (attached for vm01) Create
resource "azurerm_network_interface" "nic-win11-01" {
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
name = "${var.nic_name_pre}${var.vm_name_pre}${var.vm_name_suf01}"
ip_configuration {
name = "${var.nic_name_pre}${var.vm_name_pre}${var.vm_name_suf01}${var.ipconf_suf}"
subnet_id = azurerm_subnet.sn-client01.id
private_ip_address_allocation = "${var.priv_ip_alloc}"
}
}
# windows vm01 Create
resource "azurerm_windows_virtual_machine" "vm-win11-01" {
name = "${var.vm_name_pre}${var.vm_name_suf01}"
admin_username = "${var.adminuser}"
# admin_password = random_password.password.result
admin_password = "P@ssw0rd0123"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
network_interface_ids = [azurerm_network_interface.nic-win11-01.id]
size = "${var.vm_sku}"
priority = "Spot" # or Regular
eviction_policy = "Deallocate"
os_disk {
name = "${var.vm_osdisk_pre}${var.vm_name_pre}${var.vm_name_suf01}"
caching = "ReadWrite"
storage_account_type = "${var.vm_osdisk_sku}"
}
source_image_reference {
publisher = "${var.os_pub01}"
offer = "${var.os_offer01}"
sku = "${var.os_sku01}"
version = "${var.os_ver}"
}
boot_diagnostics {
storage_account_uri = azurerm_storage_account.storage_account.primary_blob_endpoint
}
depends_on = [
azurerm_subnet_network_security_group_association.nsg-sn-client01-to-sn-client01
]
}
# --------------------------------------
# Create Almalinux
# --------------------------------------
# nic02 (attached for vm01) Create
resource "azurerm_network_interface" "nic-alma-01" {
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
name = "${var.nic_name_pre}${var.vm_name_pre}${var.vm_name_suf02}"
ip_configuration {
name = "${var.nic_name_pre}${var.vm_name_pre}${var.vm_name_suf02}${var.ipconf_suf}"
subnet_id = azurerm_subnet.sn-client01.id
private_ip_address_allocation = "${var.priv_ip_alloc}"
}
}
# linux vm01 Create
resource "azurerm_linux_virtual_machine" "vm-alma-01" {
name = "${var.vm_name_pre}${var.vm_name_suf02}"
admin_username = "${var.adminuser}"
admin_password = "P@ssw0rd0123"
disable_password_authentication = false
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
network_interface_ids = [azurerm_network_interface.nic-alma-01.id]
size = "${var.vm_sku}"
os_disk {
name = "${var.vm_osdisk_pre}${var.vm_name_pre}${var.vm_name_suf02}"
caching = "ReadWrite"
storage_account_type = "${var.vm_osdisk_sku}"
}
source_image_reference {
publisher = "${var.os_pub02}"
offer = "${var.os_offer02}"
sku = "${var.os_sku02}"
version = "${var.os_ver}"
}
computer_name = "${var.vm_name_pre}${var.vm_name_suf02}"
boot_diagnostics {
storage_account_uri = azurerm_storage_account.storage_account.primary_blob_endpoint
}
depends_on = [
azurerm_subnet_network_security_group_association.nsg-sn-client01-to-sn-client01
]
}
はい。
ここでもちゃんとSpot Instance使っています!
vpn_connection.tf
# ------------------------------
# VPN 接続作成
# ------------------------------
# ------------------------------
# AWS VPN 接続作成
# ------------------------------
resource "aws_vpn_connection" "vpn_connection" {
vpn_gateway_id = aws_vpn_gateway.vgw.id
customer_gateway_id = aws_customer_gateway.cgw.id
type = "ipsec.1"
static_routes_only = true
tunnel1_preshared_key = "Passw0rd0123"
tags = {
Name = "${var.env_type}-${var.sys_name}-vpn-connection"
}
}
resource "aws_vpn_connection_route" "azure-vnet" {
destination_cidr_block = "${var.vnet_addr_pre01}${var.vnet_addr_suf01}"
vpn_connection_id = aws_vpn_connection.vpn_connection.id
}
# ------------------------------
# Azure VPN 接続作成
# ------------------------------
resource "azurerm_virtual_network_gateway_connection" "vpn-connection" {
name = "${var.connection_name_pre}${var.vnet_name_pre}${var.vnet_name_suf01}-${var.LG_name_pre}${var.LG_name_suf}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
type = "IPsec"
virtual_network_gateway_id = azurerm_virtual_network_gateway.vgw01.id
local_network_gateway_id = azurerm_local_network_gateway.lgw.id
shared_key = "Passw0rd0123"
}
はい。
ここのコードはAzureとAWS両方一緒に書いています。
どちらもVPN Connectionというリソース名だったので、まぁ良いかなと思い一緒に記載しました。
この設定値は言わば表裏一体、PSKやIKEのバージョンなど双方で設定値を合わせないといけない設定項目が多いため同一のファイルでコードを記載した方がわかりやすいという利点もあります。
ここで注意したいのはPSKです。
AWSは記号が使えず、Azureは記号が使えます。
なのでAWSの仕様に引っ張られてAzureはPSKで記号が使えなくなりました。
小さいですがこれくらいですね。
output.tf
# ---------------------------
# Output
# ---------------------------
# AWS VPN Cpnnection のTunnel 1のパブリックIPアドレスを出力
output "aws_vpn_gateway_tunnel1_global_ip_address" {
value = aws_vpn_connection.vpn_connection.tunnel1_address
}
# Azure 仮想ネットワーク ゲートウェイのパブリックIPアドレスを出力
output "azure_vpn_gateway_global_ip_address" {
value = "${data.azurerm_public_ip.pip-vgw.ip_address}"
}
# Azure Storage Account Nameを出力
output "diag_storage_account" {
value = azurerm_storage_account.storage_account.name
}
Outputですが、これもまぁ何にもなくても良かったんですが、今回はAzure、AWS双方のVPN GatewayのPublic IP Addressを出力しました。
あとはStorage Account名です。
動作確認
普段変数で呼んでいるAzureの認証関連の情報ですが、VS Codeでは呼べなかったのでPower Shellでいきます。
(色々調べてみたんですが、解決していません・・・)
terraform apply
したら問題なさそうなのでそのままyes
を入力します。
Azure VPN Gateway構築
GUIで確認しても構築中ですね。
そしてAWS側も
こんな感じで仮想プライベートゲートウェイができていますね。
そして・・・
今回は15分もかからず6分30秒で済みましたね。
ではちゃんとPublic IP Addressが取得できているでしょうか?
Azure VPN GatewayのPublic IP Address確認
まずはAzure Portalから
はい。
ちゃんとPublic IP Addressが取得できていて、Azure VPN Gatewayに関連付いていますね。
文句なしです。
AWS Customer GaetwayのPublic IP Address確認
はい!
完璧ですね!
やっぱりTerraformは凄い!
ちょっと集中線がうるさいの元画像もおいときます。
Terraform完了確認
はい。
ここでも確認できていますね。
ついでにAWSの仮想プライベートゲートウェイのPublic IP Address(厳密にはSite to Site VPN接続の設定時に割り当てられるPublic IP Address)も見てみましょう。
Terraformだとこのオレンジ色の囲いの通りですが、AWS Consoleだと
こっちも完璧ですね!
やっぱりTerraformは凄い!
VPN接続確認
はい。
ではいよいよ本丸です。
ちゃんとAzure-AWS間でVPN接続はできているのでしょうか?
まずはAWS Console!
そしてAzure Portal!
世界よ刮目せよ!
これがTerraformの実力だ!
いや、ホント、マジで凄い!
便利!
これぞ人類の英知の結晶ですね!
はい。
興奮しすぎてまたしても集中線がうるさいので、それぞれ集中線なしの画像も載せておきます。
通信確認
はい。
ここまででGUI上でのAzure-AWS間のVPN接続確認はできましたが、果たして実際の通信はできるのか?
という疑問が残りますね。
当然検証します!
Fleet ManagerでAWS側のWindows EC2にログインします。
と思ったら・・・
何!?
Fleet Managerを更新しただと!?
何がどう変わったかわかりませんが、Public Cloudあるあるですね。
納品直前にUIがごっそり変わってしまってGUIの手順書が役に立たなくなる・・・っていうやつですね。
では気を取り直して通信確認していきましょう。
Fleet ManagerでWindows EC2のログインし、コマンドプロンプトとRDPクライアントの画面を立ち上げます。
Azure VMのAlmalinuxが192.168.201.4
Azure VMのWindows 11が192.168.201.5
となっていますね。
それぞれにWindows EC2からアクセスしてみると・・・
やった!
通信できた!
完璧ですね!
まずはAlmalinuxログインします。
はい。
言うこと無いですね。
そうこうしてるうちにAzure VMのWindows 11にもログインでき、
やべーな。
完璧だ。
もう語彙力どっかいっちゃいましたよ。
はい。
もう何も問題ないですね。
完璧としか言いようがないです。
皆様どうでしょうか?
Terraform、マジ便利!マジ神!
はい。
ということでとても面白い実験でした。
本日はここまで。