테라폼

4-5. 테라폼 - EC2 구현하기

pininini 2026. 5. 12. 15:45

테라폼 - EC2 구현하기

Subnet, Security Group, IAM Role, User Data까지 연결해서 EC2 이해하기


이전 글에서는 IAM Role과 Policy를 Terraform으로 구현하는 방법을 정리했다.

이번 글에서는 AWS에서 가장 기본적인 컴퓨팅 리소스인 EC2를 Terraform으로 구현해보려 한다.

EC2는 처음 보면 단순히 “서버 한 대”처럼 보인다. 하지만 실제로 Terraform으로 EC2를 만들다 보면 여러 리소스가 함께 연결된다.

VPC
Subnet
Security Group
Key Pair
IAM Instance Profile
AMI
EBS
User Data
Public IP / Private IP

즉, EC2는 단독으로 존재하는 리소스가 아니라 네트워크, 보안, 권한, 스토리지와 함께 구성되는 리소스다.

EC2는 서버처럼 보이지만, Terraform에서는 여러 리소스와 연결된 교체 가능한 인프라 리소스다.

목차

  • 1. EC2란 무엇인가
  • 2. EC2를 만들 때 필요한 요소
  • 3. Public EC2와 Private EC2
  • 4. AMI 조회하기
  • 5. Key Pair 설정하기
  • 6. 기본 EC2 생성 코드
  • 7. IAM Instance Profile 연결하기
  • 8. User Data로 초기 설정하기
  • 9. EBS 설정하기
  • 10. Elastic IP 연결하기
  • 11. EC2 replacement와 private IP 변경 문제
  • 12. EC2 의존성 흐름
  • 13. 자주 하는 실수
  • 14. 마무리

1. EC2란 무엇인가

EC2는 Elastic Compute Cloud의 약자다.

쉽게 말하면 AWS에서 제공하는 가상 서버다.

EC2를 사용하면 직접 물리 서버를 구매하지 않고도 필요한 사양의 서버를 빠르게 생성할 수 있다.

EC2
→ AWS에서 생성하는 가상 서버

EC2는 다음과 같은 용도로 사용할 수 있다.

웹 서버
API 서버
배치 서버
관리자용 서버
Bastion 서버
테스트용 서버
모니터링 서버

하지만 EC2 하나만 만든다고 서비스가 완성되는 것은 아니다.

EC2가 정상적으로 동작하려면 네트워크, 보안, 권한, 스토리지 설정이 함께 필요하다.


2. EC2를 만들 때 필요한 요소

EC2를 Terraform으로 만들 때 기본적으로 다음 요소를 고려해야 한다.

요소 설명
AMI EC2에 설치될 OS 이미지
Instance Type CPU, 메모리 등 서버 사양
Subnet EC2가 배치될 네트워크 구역
Security Group EC2의 inbound / outbound 제어
Key Pair SSH 접속에 사용할 공개키
IAM Instance Profile EC2가 AWS API를 호출할 때 사용할 권한
User Data EC2 최초 부팅 시 실행할 스크립트
EBS EC2에 연결되는 디스크

Terraform 코드에서는 보통 다음처럼 연결된다.

aws_instance
├── ami
├── instance_type
├── subnet_id
├── vpc_security_group_ids
├── key_name
├── iam_instance_profile
├── user_data
└── root_block_device

3. Public EC2와 Private EC2

EC2는 어떤 Subnet에 배치하느냐에 따라 Public EC2와 Private EC2로 나눌 수 있다.

구분 특징 사용 예
Public EC2 Public Subnet에 위치하고 Public IP를 가질 수 있음 간단한 웹 서버, Bastion 서버
Private EC2 Private Subnet에 위치하고 외부에서 직접 접근하지 않음 내부 API 서버, 배치 서버, 관리 서버

Public EC2가 인터넷과 통신하려면 다음 조건이 필요하다.

1. Public Subnet에 위치
2. Public IP 또는 Elastic IP 보유
3. Route Table에 0.0.0.0/0 → Internet Gateway 경로 존재
4. Security Group / NACL 허용

Private EC2는 보통 Public IP가 없다.

Private EC2가 외부 인터넷으로 나가야 한다면 NAT Gateway나 VPC Endpoint 같은 별도 경로를 고려해야 한다.

Private EC2
→ NAT Gateway
→ Internet Gateway
→ Internet

초보자 실습에서는 Public EC2를 먼저 만들어보는 것이 이해하기 쉽다.


4. AMI 조회하기

EC2를 만들려면 AMI가 필요하다.

AMI는 Amazon Machine Image의 약자로, EC2에 설치할 OS 이미지라고 보면 된다.

예를 들어 Amazon Linux 2 최신 AMI를 조회하려면 다음처럼 작성할 수 있다.

data "aws_ami" "amazon_linux_2" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

여기서 data는 Terraform이 직접 생성하지 않고, AWS에 이미 존재하는 값을 조회할 때 사용한다.

EC2에서는 다음처럼 사용한다.

ami = data.aws_ami.amazon_linux_2.id

AMI ID를 직접 하드코딩할 수도 있지만, 리전마다 AMI ID가 다르고 시간이 지나면 바뀔 수 있다.

따라서 학습이나 간단한 템플릿에서는 data "aws_ami"를 사용해 조회하는 방식이 편하다.


5. Key Pair 설정하기

EC2에 SSH로 접속하려면 Key Pair를 사용할 수 있다.

Terraform에서는 공개키를 AWS에 등록하고, EC2에 해당 Key Pair 이름을 연결한다.

먼저 로컬에서 키를 만들 수 있다.

ssh-keygen -t ed25519 -f ~/.ssh/demo-ec2

그러면 다음 파일이 생성된다.

~/.ssh/demo-ec2      → private key
~/.ssh/demo-ec2.pub  → public key

Terraform에는 public key만 등록한다.

resource "aws_key_pair" "ec2" {
  key_name   = "${var.project_name}-ec2-key"
  public_key = file(var.public_key_path)

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ec2-key"
  })
}

변수는 다음처럼 정의할 수 있다.

variable "public_key_path" {
  description = "Path to public key file"
  type        = string
}

terraform.tfvars에는 다음처럼 입력한다.

public_key_path = "~/.ssh/demo-ec2.pub"

주의할 점은 private key를 Git에 올리면 안 된다는 것이다.

# 절대 Git에 올리면 안 됨
~/.ssh/demo-ec2
Terraform에는 public key만 등록하고, private key는 로컬에서 안전하게 관리해야 한다.

6. 기본 EC2 생성 코드

이제 가장 기본적인 EC2를 만들어보자.

이 예제는 Public Subnet에 EC2를 생성하고, Security Group과 Key Pair를 연결한다.


6.1 variables.tf

variable "project_name" {
  description = "Project name"
  type        = string
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

variable "public_key_path" {
  description = "Path to public key file"
  type        = string
}

6.2 terraform.tfvars

project_name    = "demo"
instance_type   = "t3.micro"
public_key_path = "~/.ssh/demo-ec2.pub"

6.3 EC2 코드

resource "aws_instance" "app" {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.ec2.id]
  key_name               = aws_key_pair.ec2.key_name

  associate_public_ip_address = true

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ec2"
  })
}

각 설정의 의미는 다음과 같다.

설명 의미
ami EC2에 사용할 OS 이미지
instance_type EC2 사양
subnet_id EC2가 배치될 Subnet
vpc_security_group_ids EC2에 연결할 Security Group
key_name SSH 접속에 사용할 Key Pair
associate_public_ip_address Public IP 자동 할당 여부

6.4 SSH 접속

EC2가 생성되면 output으로 public IP를 확인한다.

terraform output -raw ec2_public_ip

SSH 접속은 다음처럼 할 수 있다.

ssh -i ~/.ssh/demo-ec2 ec2-user@EC2_PUBLIC_IP

Amazon Linux 계열은 기본 사용자가 보통 ec2-user다.


7. IAM Instance Profile 연결하기

EC2 내부 애플리케이션이 AWS API를 호출해야 하는 경우가 있다.

예를 들어 다음과 같은 작업이다.

S3 객체 읽기
Secrets Manager 값 읽기
SSM Parameter 조회
CloudWatch Logs 전송

이때 Access Key를 EC2 안에 넣는 것은 권장하지 않는다.

대신 IAM Role을 만들고, Instance Profile을 통해 EC2에 연결한다.

EC2
→ Instance Profile
→ IAM Role
→ Permission Policy

7.1 IAM Role

resource "aws_iam_role" "ec2" {
  name = "${var.project_name}-ec2-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ec2-role"
  })
}

7.2 Instance Profile

resource "aws_iam_instance_profile" "ec2" {
  name = "${var.project_name}-ec2-profile"
  role = aws_iam_role.ec2.name
}

7.3 EC2에 연결

resource "aws_instance" "app" {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.ec2.id]
  key_name               = aws_key_pair.ec2.key_name

  iam_instance_profile = aws_iam_instance_profile.ec2.name

  associate_public_ip_address = true

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ec2"
  })
}

이제 EC2 내부에서는 해당 Role 권한으로 AWS API를 호출할 수 있다.

EC2에서 AWS API를 호출해야 한다면 Access Key를 넣지 말고 IAM Role을 붙이는 것이 좋다.

8. User Data로 초기 설정하기

User Data는 EC2가 처음 생성될 때 실행되는 초기화 스크립트다.

예를 들어 EC2 생성 시 Apache 웹 서버를 설치하고 간단한 페이지를 만들 수 있다.

resource "aws_instance" "app" {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.ec2.id]
  key_name               = aws_key_pair.ec2.key_name

  associate_public_ip_address = true

  user_data = <<-EOF
              #!/bin/bash
              yum update -y
              yum install -y httpd
              systemctl enable httpd
              systemctl start httpd
              echo "Hello Terraform EC2" > /var/www/html/index.html
              EOF

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ec2"
  })
}

이후 브라우저에서 다음 주소로 접속하면 된다.

http://EC2_PUBLIC_IP

단, Security Group에서 80번 포트가 열려 있어야 한다.

User Data에 대해 주의할 점이 있다.

User Data는 일반적으로 최초 부팅 시 실행된다.
기존 EC2의 user_data를 바꿨다고 해서 항상 스크립트가 다시 실행되는 것은 아니다.

따라서 애플리케이션 배포나 반복 실행이 필요한 설정은 User Data만으로 처리하기보다, CI/CD, Ansible, SSM Run Command, 이미지 빌드 방식 등을 고려할 수 있다.

Terraform에서 User Data 변경 시 EC2 교체를 의도적으로 유도하고 싶다면 user_data_replace_on_change 같은 옵션을 고려할 수 있다.

user_data_replace_on_change = true

다만 이 경우 User Data 변경이 EC2 replacement로 이어질 수 있으므로 운영 환경에서는 주의해야 한다.


9. EBS 설정하기

EC2에는 기본적으로 root volume이 연결된다.

Terraform에서는 root_block_device로 root volume 설정을 조정할 수 있다.

resource "aws_instance" "app" {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.ec2.id]

  root_block_device {
    volume_size           = 20
    volume_type           = "gp3"
    delete_on_termination = true
    encrypted             = true
  }

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ec2"
  })
}

주요 설정은 다음과 같다.

설정 의미
volume_size 디스크 크기
volume_type EBS 타입
delete_on_termination EC2 삭제 시 root volume도 삭제할지 여부
encrypted EBS 암호화 여부

추가 EBS Volume을 별도로 만들고 EC2에 붙일 수도 있다.

resource "aws_ebs_volume" "data" {
  availability_zone = aws_instance.app.availability_zone
  size              = 20
  type              = "gp3"
  encrypted         = true

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-data-volume"
  })
}

resource "aws_volume_attachment" "data" {
  device_name = "/dev/sdf"
  volume_id   = aws_ebs_volume.data.id
  instance_id = aws_instance.app.id
}

추가 EBS Volume은 EC2와 별도 생명주기를 가질 수 있다.

다만 EC2 교체나 삭제 시 EBS를 어떻게 유지할지 명확히 설계해야 한다.


10. Elastic IP 연결하기

EC2에 자동 할당되는 Public IP는 EC2가 중지되거나 교체되면 바뀔 수 있다.

고정 Public IP가 필요하다면 Elastic IP를 사용할 수 있다.

resource "aws_eip" "app" {
  domain = "vpc"

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ec2-eip"
  })
}

resource "aws_eip_association" "app" {
  instance_id   = aws_instance.app.id
  allocation_id = aws_eip.app.id
}

Elastic IP를 사용하면 EC2의 Public IP를 고정할 수 있다.

하지만 주의할 점이 있다.

Elastic IP는 public IP 고정에 사용한다.
private IP 고정 문제를 해결하는 리소스는 아니다.

또한 사용하지 않는 Elastic IP는 비용이 발생할 수 있으므로 실습 후 정리해야 한다.


11. EC2 replacement와 private IP 변경 문제

EC2를 Terraform으로 운영하다 보면 중요한 문제를 만날 수 있다.

바로 EC2 replacement다.

Terraform에서 어떤 변경은 기존 EC2를 그대로 수정하는 방식으로 처리된다.

in-place update

반면 어떤 변경은 기존 EC2를 삭제하고 새 EC2를 만드는 방식으로 처리된다.

replacement

Terraform plan에서 replacement는 보통 다음처럼 보인다.

-/+ aws_instance.app must be replaced

이 경우 기존 EC2가 사라지고 새 EC2가 생성될 수 있다.

문제는 EC2가 새로 생성되면서 동적으로 할당된 값들이 바뀔 수 있다는 점이다.

instance_id
private_ip
public_ip
network_interface_id

11.1 실제로 생길 수 있는 문제

예를 들어 EC2가 자동 할당 private IP를 사용한다고 하자.

기존 EC2 private IP
10.0.1.25

다른 리소스나 설정이 이 IP를 직접 바라보고 있을 수 있다.

다른 서버 설정
backend = 10.0.1.25

그런데 IAM Instance Profile, 네트워크 설정, AMI, User Data 교체 설정 등으로 EC2 replacement가 발생하면 새 EC2가 생성된다.

새 EC2 private IP
10.0.1.87

그러면 기존 IP를 바라보던 리소스는 새 EC2를 찾지 못한다.

EC2 replacement
→ private IP 변경
→ 기존 연결 대상이 사라짐
→ 서비스 연결 실패

이 문제의 핵심은 다음이다.

동적으로 할당된 private IP를 안정적인 연결 지점처럼 사용하면 위험하다.

11.2 해결 방법 1: private_ip 고정

가장 단순한 방법은 EC2에 private IP를 명시하는 것이다.

resource "aws_instance" "app" {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.private.id
  vpc_security_group_ids = [aws_security_group.app.id]

  private_ip = "10.0.2.10"

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ec2"
  })
}

이렇게 하면 EC2가 재생성될 때 같은 private IP를 다시 사용하려고 한다.

다만 주의할 점이 있다.

기존 EC2가 해당 IP를 아직 점유 중이면
새 EC2 생성이 실패할 수 있다.

즉, 고정 private IP는 같은 IP 유지에는 유리하지만, 무중단 교체에는 불리할 수 있다.


11.3 해결 방법 2: ENI를 별도 리소스로 분리

더 명확한 방식은 private IP를 EC2가 아니라 ENI에 묶는 것이다.

ENI는 Elastic Network Interface의 약자로, EC2에 연결되는 네트워크 인터페이스다.

resource "aws_network_interface" "app" {
  subnet_id       = aws_subnet.private.id
  private_ips     = ["10.0.2.10"]
  security_groups = [aws_security_group.app.id]

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-app-eni"
  })
}

resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = var.instance_type

  iam_instance_profile = aws_iam_instance_profile.ec2.name

  network_interface {
    network_interface_id = aws_network_interface.app.id
    device_index         = 0
  }

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-ec2"
  })
}

이 구조는 네트워크 정체성과 컴퓨팅 리소스를 분리한다.

네트워크 정체성
→ ENI

서버 실행 리소스
→ EC2

다만 ENI를 주 네트워크 인터페이스로 사용하는 경우에도 attach/detach 순서와 replacement 동작을 잘 확인해야 한다.


11.4 해결 방법 3: IP 대신 DNS 또는 Load Balancer 사용

가능하면 다른 리소스가 EC2 private IP를 직접 바라보지 않게 하는 것이 좋다.

EC2의 private IP는 EC2가 교체되거나 재생성될 때 바뀔 수 있다. 따라서 다른 리소스가 private IP를 직접 참조하면 EC2 replacement 시 연결이 깨질 수 있다.

위험한 구조:
Other Service → 10.0.2.10

EC2 replacement 발생:
10.0.2.10 → 10.0.2.35

결과:
Other Service는 여전히 10.0.2.10을 바라봄

이 문제를 줄이려면 IP를 직접 참조하기보다, 중간에 안정적인 연결 지점을 두는 것이 좋다.

권장 구조:
Other Service → DNS / Load Balancer / Service Discovery → EC2

11.4.1 DNS를 사용하는 방법

먼저 DNS를 사용할 수 있다.

예를 들어 Route53 Private Hosted Zone을 사용하면 내부 도메인 이름을 만들 수 있다.

app.internal.example.com
→ EC2 private IP

Terraform 예시는 다음과 같다.

resource "aws_route53_record" "app" {
  zone_id = aws_route53_zone.private.zone_id
  name    = "app.internal.example.com"
  type    = "A"
  ttl     = 60
  records = [aws_instance.app.private_ip]
}

이렇게 하면 다른 리소스는 EC2의 private IP를 직접 알 필요가 없다.

Before:
Other Service → 10.0.2.10

After:
Other Service → app.internal.example.com

EC2가 교체되어 private IP가 바뀌더라도, 다른 리소스의 설정을 모두 수정하는 대신 DNS Record만 갱신하면 된다.

기존:
app.internal.example.com → 10.0.2.10

변경:
app.internal.example.com → 10.0.2.35

다만 DNS 방식도 완전한 즉시 전환을 보장하지는 않는다.

DNS에는 TTL과 캐시가 있기 때문이다.

DNS Record 변경
→ 일부 클라이언트는 기존 IP 캐시 사용
→ TTL 이후 새 IP 조회

예를 들어 TTL이 60초라면, 일부 클라이언트는 최대 60초 정도 기존 IP를 계속 사용할 수 있다.

따라서 DNS는 IP 직접 참조를 줄이는 데는 좋지만, 즉시 전환이나 무중단 배포가 필요한 경우에는 한계가 있다.

DNS는 IP 변경의 영향을 줄여주지만, TTL과 캐시 때문에 즉시 전환을 보장하지는 않는다.

11.4.2 Load Balancer를 사용하는 방법

더 안정적인 방식은 EC2 앞에 Load Balancer를 두는 것이다.

Other Service
→ Internal ALB / NLB
→ EC2

이 구조에서는 다른 리소스가 EC2를 직접 바라보지 않는다.

다른 리소스는 Load Balancer를 바라보고, Load Balancer가 뒤쪽의 EC2로 트래픽을 전달한다.

Before:
Other Service → EC2 private IP

After:
Other Service → Internal Load Balancer → EC2

이 방식의 장점은 EC2가 교체되어도 클라이언트의 연결 지점이 유지된다는 것이다.

EC2가 새로 생성되면 Target Group에 새 EC2를 등록하고, 기존 EC2를 제거하면 된다.

기존 EC2
→ Target Group에서 제거

새 EC2
→ Target Group에 등록

Other Service
→ 여전히 Load Balancer만 바라봄

또한 Load Balancer는 Health Check를 통해 정상 상태의 대상에게만 트래픽을 보낼 수 있다.

정상 EC2
→ 트래픽 전달

비정상 EC2
→ 트래픽 제외

따라서 무중단 배포, 장애 대응, 여러 EC2로의 트래픽 분산이 필요하다면 DNS만 사용하는 것보다 Load Balancer 구조가 더 적합하다.


11.4.3 DNS와 Load Balancer 비교

구분 DNS Load Balancer
역할 IP를 이름으로 추상화 트래픽을 대상 리소스로 분산
EC2 IP 변경 대응 DNS Record 갱신 필요 Target Group 대상 교체
전환 속도 TTL / 캐시 영향 있음 Health Check 기반으로 비교적 빠르게 전환 가능
Health Check 기본 DNS만으로는 어려움 가능
비용 상대적으로 낮음 추가 비용 발생
적합한 경우 단순 내부 이름 관리 무중단, 확장성, 장애 대응이 필요한 서비스

정리하면 DNS와 Load Balancer는 둘 다 EC2 private IP 직접 참조를 줄이는 방법이다.

다만 목적과 안정성 수준이 다르다.

DNS
→ IP를 이름으로 추상화
→ 단순하고 비용 부담이 적음
→ TTL / 캐시 고려 필요

Load Balancer
→ EC2 교체를 뒤에서 흡수
→ Health Check 가능
→ 무중단 배포와 확장에 유리
→ 비용 증가

소규모 내부 도구나 단순 연결에는 DNS만으로 충분할 수 있다.

하지만 서비스 안정성, 무중단 배포, 장애 대응이 중요하다면 Load Balancer를 사용하는 것이 더 적합하다.

IP를 직접 참조하지 말고, DNS나 Load Balancer 같은 안정적인 연결 지점을 두는 것이 좋다.

11.5 해결 방법 4: prevent_destroy로 실수 방지

의도치 않은 EC2 교체를 막고 싶다면 prevent_destroy를 사용할 수 있다.

resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = var.instance_type

  lifecycle {
    prevent_destroy = true
  }
}

이 설정은 Terraform이 해당 리소스를 삭제하려고 할 때 오류를 발생시킨다.

단, 이것은 구조적인 해결책이라기보다 안전장치에 가깝다.

prevent_destroy
→ 실수 방지용

DNS / LB / ENI 분리
→ 구조적 해결책

11.6 IAM 변경은 profile 교체보다 policy 변경 중심으로

EC2에 연결된 Instance Profile 자체를 자주 바꾸면 EC2 변경 영향이 커질 수 있다.

가능하면 EC2에 붙는 Instance Profile은 안정적으로 유지하고, 권한 변경은 Role에 연결된 Policy를 수정하는 방식이 더 안전하다.

권장 방향:
EC2 → 같은 Instance Profile 유지
Role Policy 변경으로 권한 조정

예를 들어 다음처럼 Role 자체는 유지하고 Policy Attachment를 변경한다.

resource "aws_iam_role_policy_attachment" "s3_read" {
  role       = aws_iam_role.ec2.name
  policy_arn = aws_iam_policy.s3_read.arn
}

물론 어떤 변경이 실제로 replacement를 유발하는지는 Terraform Provider 버전과 리소스 구성에 따라 달라질 수 있다.

따라서 항상 terraform plan에서 다음 표시를 확인해야 한다.

~   in-place update
-/+ replacement

12. EC2 의존성 흐름

EC2는 여러 리소스를 참조한다.

VPC
→ Subnet
→ Security Group
→ IAM Instance Profile
→ Key Pair
→ EC2

하지만 실제 의존성은 트리보다 그래프에 가깝다.

 

 

EC2는 Subnet, Security Group, AMI, Key Pair, Instance Profile을 모두 참조한다.

따라서 EC2를 단순 서버 하나로 보기보다 여러 리소스가 결합된 실행 리소스로 보는 것이 좋다.


13. 자주 하는 실수

13.1 Public IP만 있으면 인터넷이 된다고 생각함

Public IP는 인터넷 통신을 위한 조건 중 하나일 뿐이다.

다음 조건이 함께 필요하다.

Public IP
Internet Gateway
Route Table
Security Group
NACL

Public IP가 있어도 Route Table에 Internet Gateway 경로가 없으면 인터넷 통신이 되지 않는다.


13.2 Security Group에서 SSH를 전체 공개함

SSH를 전체 인터넷에 열어두는 것은 위험하다.

# 권장하지 않음
cidr_ipv4 = "0.0.0.0/0"
from_port = 22
to_port   = 22

가능하면 본인의 IP만 허용한다.

cidr_ipv4 = "1.2.3.4/32"

더 좋은 방식은 SSH 대신 SSM Session Manager를 사용하는 것이다.


13.3 Access Key를 EC2에 직접 넣음

EC2에서 AWS API를 호출해야 한다고 해서 Access Key를 코드나 환경 변수에 직접 넣으면 안 된다.

# 권장하지 않음
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...

대신 IAM Role과 Instance Profile을 사용한다.


13.4 User Data를 배포 도구처럼 사용함

User Data는 초기 부팅 스크립트에 가깝다.

기존 EC2의 애플리케이션을 반복적으로 배포하는 도구로 사용하기에는 한계가 있다.

반복 배포가 필요하다면 CI/CD, SSM Run Command, 이미지 빌드, 구성 관리 도구 등을 고려하는 것이 좋다.


13.5 EC2 private IP를 고정 식별자로 사용함

EC2의 private IP를 명시하지 않으면 AWS가 자동으로 할당한다.

EC2 replacement가 발생하면 private IP가 바뀔 수 있다.

따라서 다른 리소스가 EC2 private IP를 직접 바라보는 구조는 주의해야 한다.

위험한 구조:
Other Service → EC2 private IP 직접 참조

더 나은 구조:
Other Service → DNS / Load Balancer / Service Discovery

13.6 terraform plan에서 replacement를 확인하지 않음

Terraform plan에서 다음 표시는 매우 중요하다.

~   in-place update
-/+ replacement

EC2에 -/+가 표시된다면 새 EC2가 생성되고 기존 EC2가 삭제될 수 있다.

이 경우 IP, instance id, attached resource 영향까지 확인해야 한다.


13.7 실습 후 EC2를 삭제하지 않음

EC2는 실행 중이면 비용이 발생한다.

실습 후에는 반드시 삭제한다.

terraform destroy

Elastic IP, EBS Volume도 함께 정리되었는지 확인하는 것이 좋다.


14. 마무리

이번 글에서는 Terraform으로 EC2를 구현하는 방법을 정리했다.

EC2는 단순히 서버 하나를 만드는 리소스처럼 보이지만, 실제로는 여러 요소가 함께 연결된다.

AMI
Instance Type
Subnet
Security Group
Key Pair
IAM Instance Profile
User Data
EBS
Public IP / Private IP

또한 Terraform에서는 EC2가 replacement될 수 있다는 점도 중요하다.

EC2가 교체되면 private IP나 public IP가 바뀔 수 있고, 이를 직접 바라보던 다른 리소스가 연결을 잃을 수 있다.

따라서 EC2를 안정적으로 운영하려면 다음 관점을 가져야 한다.

EC2 private IP를 직접 연결 대상으로 삼지 않는다.
가능하면 DNS, Load Balancer, Service Discovery를 사용한다.
IAM 권한은 Access Key가 아니라 Role로 부여한다.
terraform plan에서 replacement 여부를 반드시 확인한다.

한 줄 정리

EC2는 서버처럼 보이지만, 네트워크·보안·권한·스토리지가 결합된 교체 가능한 리소스다.


다음 글에서는 S3를 Terraform으로 구현해본다. S3는 단순 저장소처럼 보이지만 Public Access Block, Versioning, Encryption, Lifecycle, Bucket Policy를 함께 고려해야 하는 리소스다.