Terraform으로 Azure Bastion Host 구축하기 | How to build Azure Bastion Host by Terraform?
Create Azure Virtual Machine Use Terraform
Terraform으로 기본적인 IaaS 구축을 진행합니다. (모듈 사용 X)
테스트 환경
- PC: Apple MacBook Air 2020(M1)
- OS: macOS Monterey 12.3.1
- IDE: Visual Studio Code 1.66.1
- Terraform: v1.1.8 (on darwin_arm64), brew install
- Azure CLI: stable 2.35.0 (Microsoft Azure CLI 2.0)
전체 구성도

Azure VM 접속 방식
- Password로 접근하는 방식
- SSH Key를 이용하여 접근하는 방식
2가지가 있고 이 글에서는 SSH Key를 이용하여 접근하는 방식을 사용합니다.
ssh-keygen을 이용하여 키를 생성하는 방법은 아래를 참고하시면됩니다.
$ ssh-keygen
# WorkSpace 디렉터리 지정
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/user/.ssh/id_rsa): /WorkSpace/KP/testvm-public
# 비워 두시면 됩니다.
Enter passphrase (empty for no passphrase):
Your identification has been saved in /WorkSpace/KP/testvm-public
Your public key has been saved in /WorkSpace/KP/testvm-public.pub
The key fingerprint is:
SHA256:Oc/6iqU5MI1JkOjuws3WRupObvo0L/lBEYLD5kwAu/o
The key's randomart image is:
+---[RSA 3072]----+
|*..o . |
|.Bo . . |
|B .. . |
| = . . . |
|o . = S |
|.. *.. + |
|+ o+=+ . o |
|.++Oooo= . |
|..E=+o+.oo. |
+----[SHA256]-----+
위와 같은 방법을 사용하여 Bastion Host 와 Private VM을 위한 Key를 각각 생성해 주시면됩니다.
파일이름
- Bastion Host: testvm-public
- Private VM: testvm-private
Azure Cli 로그인
Azure 인프라를 Terraform으로 컨트롤 하려면 Azure에 로그인을 하셔야합니다.
방법은 간단합니다.
$ az login
웹사이트로 이동하고 로그인하면 cli도 로그인됩니다.
정상적으로 로그인 되었는지 확인해봅니다.
$ az account show -o json
계정과 테넌트가 정상적으로 확인되면 끝입니다.
전체 파일
- main.tf
- Provider 선언
- Resource Group 확인 및 생성
- VNet 생성
- Subnet 생성
- NAT Gateway 생성
- Resource Association
- data.tf
- Resource Group 존재여부 체크를 위한 데이터
- output.tf
- Resource Group가 기존에 있었는지 확인
- NAT Gateway Public IP 확인
- Bastion Host Public IP 확인
- sg.tf
- SSH 허용을 Security Group으로 정의(단일 리소스가 내용이 길어서 별도로 빼놨습니다.)
- test.tf
- Bastion Host 및 Private Virtual Machine 생성 정의(NIC, SSH Key 등)
Terraform Apply
코드는 제가 GitHub에 올린 파일입니다.
- main.tf
# Define Provider
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=3.2.0"
}
}
}
provider "azurerm" {
features {}
}
### Define & Creation ###
# Define Resource Group
resource "azurerm_resource_group" "resource-group" {
count = data.azurerm_resource_group.resource-group.name == var.resource-group-name ? 0 : 1
name = var.resource-group-name
location = var.azure-location
}
# Create vnet(Virtual Network)
resource "azurerm_virtual_network" "vnet" {
name = var.vnet-name
resource_group_name = data.azurerm_resource_group.resource-group.name
location = data.azurerm_resource_group.resource-group.location
address_space = [var.vnet-cidr]
tags = var.tagging
}
resource "azurerm_subnet" "public-subnet" {
name = var.public-subnet-name
resource_group_name = data.azurerm_resource_group.resource-group.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.subnet-cidrs["public"]]
}
resource "azurerm_subnet" "private-subnet" {
name = var.private-subnet-name
resource_group_name = data.azurerm_resource_group.resource-group.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.subnet-cidrs["private"]]
}
resource "azurerm_public_ip" "nat-gw-public-ip" {
name = var.public-nat-ip-name
resource_group_name = data.azurerm_resource_group.resource-group.name
location = data.azurerm_resource_group.resource-group.location
allocation_method = var.nat-gateway-allocation-method
sku = var.nat-gateway-public-sku
tags = var.tagging
}
resource "azurerm_nat_gateway" "nat-gw" {
name = var.public-nat-name
resource_group_name = data.azurerm_resource_group.resource-group.name
location = data.azurerm_resource_group.resource-group.location
tags = var.tagging
}
### Association ###
# NAT - Subnet
resource "azurerm_subnet_nat_gateway_association" "public-nat-association" {
subnet_id = azurerm_subnet.private-subnet.id
nat_gateway_id = azurerm_nat_gateway.nat-gw.id
}
# NAT - Public IP
resource "azurerm_nat_gateway_public_ip_association" "nat-public-ip-association" {
nat_gateway_id = azurerm_nat_gateway.nat-gw.id
public_ip_address_id = azurerm_public_ip.nat-gw-public-ip.id
}
# Subnet - Security Group
resource "azurerm_subnet_network_security_group_association" "public-security-group-association" {
subnet_id = azurerm_subnet.public-subnet.id
network_security_group_id = azurerm_network_security_group.public-subnet-security-group.id
}
- data.tf
data "azurerm_resource_group" "resource-group" {
name = var.resource-group-name
}
- output.tf
output "check-resource-group" {
value = data.azurerm_resource_group.resource-group.name == var.resource-group-name ? format("%s was Exist", var.resource-group-name) : format("%s was Not Exist", var.resource-group-name)
}
output "nat-gateway-public-ip" {
value = azurerm_public_ip.nat-gw-public-ip.ip_address
}
output "public-test-vm-public-ip" {
value = azurerm_public_ip.public-vm-nic-public-ip.ip_address
}
- sg.tf
resource "azurerm_network_security_group" "public-subnet-security-group" {
name = var.public-subnet-sg-name
location = data.azurerm_resource_group.resource-group.location
resource_group_name = data.azurerm_resource_group.resource-group.name
security_rule = [ {
access = "Allow"
description = "Allow SSH Inbound"
destination_address_prefix = "*"
destination_address_prefixes = null
destination_application_security_group_ids = null
destination_port_range = "22"
destination_port_ranges = null
direction = "Inbound"
name = "Allow SSH"
priority = 100
protocol = "*"
source_address_prefix = "*"
source_address_prefixes = null
source_application_security_group_ids = null
source_port_range = "*"
source_port_ranges = null
}]
tags = var.tagging
}
- test.tf
# Virtual Machine Test for Network
# Public Subnet
resource "azurerm_public_ip" "public-vm-nic-public-ip" {
name = "MSA-TEST-VM-PUBLIC-NIC-PUBLIC-IP"
resource_group_name = data.azurerm_resource_group.resource-group.name
location = data.azurerm_resource_group.resource-group.location
allocation_method = "Static"
sku = "Standard"
tags = var.tagging
}
resource "azurerm_network_interface" "public-vm-nic" {
name = "MSA-TEST-VM-PUBLIC-NIC"
resource_group_name = data.azurerm_resource_group.resource-group.name
location = data.azurerm_resource_group.resource-group.location
ip_configuration {
name = "nic-configure"
subnet_id = azurerm_subnet.public-subnet.id
private_ip_address_allocation = "Static"
private_ip_address = "192.168.0.10"
public_ip_address_id = azurerm_public_ip.public-vm-nic-public-ip.id
}
tags = var.tagging
}
resource "azurerm_ssh_public_key" "public-test-vm-public-key" {
name = "MSA-TEST-VM-PUBLIC-KEY"
resource_group_name = data.azurerm_resource_group.resource-group.name
location = data.azurerm_resource_group.resource-group.location
public_key = file("../KP/testvm-public.pub")
}
resource "azurerm_linux_virtual_machine" "public-vm" {
name = "MSA-TEST-VM-PUBLIC"
location = data.azurerm_resource_group.resource-group.location
resource_group_name = data.azurerm_resource_group.resource-group.name
size = "Standard_B2s"
network_interface_ids = [azurerm_network_interface.public-vm-nic.id]
admin_username = "vmadmin"
disable_password_authentication = true
admin_ssh_key {
username = "vmadmin"
public_key = azurerm_ssh_public_key.public-test-vm-public-key.public_key
}
source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "18.04-LTS"
version = "latest"
}
os_disk {
name = "MSA-TEST-VM-PUBLIC-OS-DISK"
caching = "None"
storage_account_type = "StandardSSD_LRS"
}
tags = var.tagging
}
# Private Subnet
resource "azurerm_network_interface" "private-vm-nic" {
name = "MSA-TEST-VM-PRIVATE-NIC"
resource_group_name = data.azurerm_resource_group.resource-group.name
location = data.azurerm_resource_group.resource-group.location
ip_configuration {
name = "nic-configure"
subnet_id = azurerm_subnet.private-subnet.id
private_ip_address_allocation = "Static"
private_ip_address = "192.168.10.10"
}
tags = var.tagging
}
resource "azurerm_ssh_public_key" "private-test-vm-public-key" {
name = "MSA-TEST-VM-PRIVATE-KEY"
resource_group_name = data.azurerm_resource_group.resource-group.name
location = data.azurerm_resource_group.resource-group.location
public_key = file("../KP/testvm-private.pub")
}
resource "azurerm_linux_virtual_machine" "private-vm" {
name = "MSA-TEST-VM-PRIVATE"
location = data.azurerm_resource_group.resource-group.location
resource_group_name = data.azurerm_resource_group.resource-group.name
size = "Standard_B2s"
network_interface_ids = [azurerm_network_interface.private-vm-nic.id]
admin_username = "vmadmin"
disable_password_authentication = true
admin_ssh_key {
username = "vmadmin"
public_key = azurerm_ssh_public_key.private-test-vm-public-key.public_key
}
source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "18.04-LTS"
version = "latest"
}
os_disk {
name = "MSA-TEST-VM-PRIVATE-OS-DISK"
caching = "None"
storage_account_type = "StandardSSD_LRS"
}
tags = var.tagging
}
- variable.tf
variable "resource-group-name" {
type = string
default = "Cloud_MSA-Dev-Test"
}
variable "azure-location" {
type = string
default = "Korea Central"
}
variable "vnet-name" {
type = string
# 이름은 2~64자 사이여야 합니다.
# 이름은 영문, 숫자, 밑줄, 마침표 또는 하이픈만 포함할 수 있습니다. 단 영문 또는 숫자로 시작하고 영문, 숫자 또는 밑줄로 끝나야 합니다.
# 값은 비어 있으면 안 됩니다.
default = "MSA-vnet"
}
variable "vnet-cidr" {
type = string
default = "192.168.0.0/16"
}
variable "internet-cidr" {
type = string
default = "0.0.0.0/0"
}
variable "public-subnet-name" {
type = string
# 이름은 영문, 숫자, 밑줄, 마침표 또는 하이픈만 포함할 수 있습니다. 단 영문 또는 숫자로 시작하고 영문, 숫자 또는 밑줄로 끝나야 합니다.
default = "public-subnet"
}
variable "public-subnet-sg-name" {
type = string
default = "MSA-public-subnet-security-group"
}
variable "private-subnet-name" {
type = string
default = "private-subnet"
}
variable "subnet-cidrs" {
type = map(string)
default = {
"public" = "192.168.0.0/24"
"private" = "192.168.10.0/24"
}
}
variable "public-nat-name" {
type = string
default = "MSA-ngw"
}
variable "public-nat-ip-name" {
type = string
default = "MSA-nat-gw-public-ip"
}
variable "nat-gateway-allocation-method" {
type = string
default = "Static"
}
variable "nat-gateway-public-sku" {
type = string
default = "Standard"
}
variable "tagging" {
type = map(string)
default = {
"Create by" = "eocis"
"terraform" = "true"
}
}
참고사항
Azure VM은 인터넷 통신을 하기위해 별도로 Route Table을 설정할 필요가 없습니다.
기본적으로 Outbound 통신이 가능합니다. (링크)
하지만 NAT Gateway를 두고 통신하는 것이 좋습니다.
AWS와는 설정하는게 달라 좀 까다롭긴했네요
또 Liunx Virtual Machine에서는 traceroute와 ping명령어가 정상적으로 동작하지 않습니다.
네트워크 테스트는 curl로 진행하시면 될 듯 하네요
- 로컬에 Docker nginx이미지를 가동하고 Azure VM에서 로컬 nginx container로 curl요청을 보냅니다.
확인은 docker logs -n 10 (Container ID) 로 확인하시면 됩니다.
$ sudo docker logs -n 10 cf4
58.150.221.203 - - [19/Apr/2022:08:50:08 +0000] "POST ~~~~~~~~~
Member discussion