7 min read

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)

전체 구성도

Image

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"
    }
}

참고사항

GitHub

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 ~~~~~~~~~