배스천은 사설 서브넷(Private Subnet)의 인스턴스에 안전하게 접속하기 위한 “점프 서버(관문)”.
운영자는 인터넷 → 배스천(퍼블릭 서브넷) → 사설 인스턴스(프라이빗 서브넷) 순서로 SSH/RDP 접속합.
Internet
│ (허용된 관리 IP만)
▼
[ Bastion (Public Subnet, EIP) ] ─── SSH/RDP ───► [ App/DB (Private Subnet) ]
│ 0.0.0.0/0 → IGW (공인 IP 없음)
문제: Private Subnet 인스턴스는 공인 IP가 없어 직접 접속이 불가.
해결: 공인 IP/EIP가 붙은 배스천에만 외부 접속을 열고, 거기서 다시 내부로 점프.
이점
자주 헷갈리는 것
입력으로 VPC/서브넷/사무실 IP 대역만 받습니다.
배스천은 퍼블릭 서브넷, 앱 인스턴스는 프라이빗 서브넷에 배치합니다.
(리전: 서울ap-northeast-2)
versions.tfterraform {
required_version = "~> 1.9"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" { region = "ap-northeast-2" }
variables.tfvariable "vpc_id" { type = string }
variable "public_subnet_id" { type = string }
variable "private_subnet_id" { type = string }
variable "office_cidr" {
description = "관리자 사무실 고정 IP 대역 (예: 203.0.113.0/24)"
type = string
default = "203.0.113.0/24"
validation {
condition = can(cidrnetmask(var.office_cidr))
error_message = "유효한 IPv4 CIDR이어야 합니다."
}
}
data&locals.tf (AMI 조회 + 공통 태그)# Amazon Linux 2023 최신 AMI (x86_64)
data "aws_ssm_parameter" "al2023" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}
locals {
name_prefix = "velog-bastion-demo"
tags = { Project = "velog", Role = "bastion-demo" }
}
security.tf (보안그룹)# 배스천: 사무실 IP만 22포트 허용
resource "aws_security_group" "bastion" {
name = "${local.name_prefix}-bastion-sg"
vpc_id = var.vpc_id
tags = local.tags
}
resource "aws_security_group_rule" "bastion_ssh_in" {
type = "ingress"
security_group_id = aws_security_group.bastion.id
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.office_cidr]
}
resource "aws_security_group_rule" "bastion_all_out" {
type = "egress"
security_group_id = aws_security_group.bastion.id
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
# Private 인스턴스: 배스천 SG에서만 22 허용
resource "aws_security_group" "private" {
name = "${local.name_prefix}-private-sg"
vpc_id = var.vpc_id
tags = local.tags
}
resource "aws_security_group_rule" "private_ssh_from_bastion" {
type = "ingress"
security_group_id = aws_security_group.private.id
from_port = 22
to_port = 22
protocol = "tcp"
source_security_group_id = aws_security_group.bastion.id
}
resource "aws_security_group_rule" "private_all_out" {
type = "egress"
security_group_id = aws_security_group.private.id
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
compute.tf (EC2 배치 + EIP)# 배스천 (Public Subnet, EIP)
resource "aws_instance" "bastion" {
ami = data.aws_ssm_parameter.al2023.value
instance_type = "t3.micro"
subnet_id = var.public_subnet_id
vpc_security_group_ids = [aws_security_group.bastion.id]
associate_public_ip_address = true
tags = merge(local.tags, { Name = "${local.name_prefix}-bastion" })
}
resource "aws_eip" "bastion" {
domain = "vpc"
tags = local.tags
}
resource "aws_eip_association" "bastion" {
instance_id = aws_instance.bastion.id
allocation_id = aws_eip.bastion.id
}
# Private 인스턴스 (공인 IP 없음)
resource "aws_instance" "app" {
ami = data.aws_ssm_parameter.al2023.value
instance_type = "t3.micro"
subnet_id = var.private_subnet_id
vpc_security_group_ids = [aws_security_group.private.id]
associate_public_ip_address = false
tags = merge(local.tags, { Name = "${local.name_prefix}-app" })
}
outputs.tfoutput "bastion_eip" { value = aws_eip.bastion.public_ip }
output "app_private_ip" { value = aws_instance.app.private_ip }
실행
terraform init
terraform plan -out=plan.bin
terraform apply plan.bin
terraform output
# mac/linux
ssh -J ec2-user@$(terraform output -raw bastion_eip) \
ec2-user@$(terraform output -raw app_private_ip)
~/.ssh/config)Host bastion
HostName <BASTION_EIP>
User ec2-user
IdentityFile ~/.ssh/id_rsa
Host app-priv
HostName <APP_PRIVATE_IP>
User ec2-user
ProxyJump bastion
ssh app-priv
Amazon Linux 기본 유저:
ec2-user. 우분투면ubuntu.
키페어(프라이빗 키) 권한은chmod 600인 것 확인!
AWS Systems Manager Session Manager
EC2 Instance Connect(또는 EIC Endpoint)
신규/보안 민감 환경이면 Session Manager 등 배스천리스(bastionless) 접근을 우선 검토하세요.
chmod 600 ~/.ssh/id_rsaec2-user, Ubuntu=ubuntu, RHEL=ec2-user/ec2-user@…배스천 = 외부 접속의 단일 관문. 퍼블릭에 배스천 1대만 열고, 프라이빗은 배스천 SG만 허용하자. 가능한 경우 Session Manager로 포트 0개 운영도 고려!