테라폼

4-1. 테라폼 - AWS 기본 프로젝트 템플릿 만들기

pininini 2026. 5. 11. 17:18

테라폼 - AWS 기본 프로젝트 템플릿 만들기

초보자가 바로 실행해볼 수 있는 Terraform AWS 기본 구조


이전 글에서는 Terraform 코드를 작성할 때 자주 사용하는 기본 파일 구조를 정리했다.

versions.tf
provider.tf
main.tf
variables.tf
terraform.tfvars
outputs.tf
locals.tf

이번 글에서는 이 파일 구조를 바탕으로, 초보자가 바로 실행해볼 수 있는 AWS 기본 Terraform 프로젝트 템플릿을 만들어보려 한다.

목표는 아주 단순하다.

VPC를 만들고
Public Subnet을 만들고
Internet Gateway와 Route Table을 연결하고
Security Group을 만든 뒤
EC2 하나를 실행한다.

이 정도만 구현해도 Terraform의 기본 흐름을 이해하기에 충분하다.

이 글의 목적은 완성형 운영 인프라를 만드는 것이 아니라, Terraform 프로젝트의 기본 구조와 실행 흐름을 익히는 것이다.

목차

  • 1. 이번 글에서 만들 구조
  • 2. 프로젝트 폴더 구조
  • 3. versions.tf
  • 4. provider.tf
  • 5. variables.tf
  • 6. terraform.tfvars
  • 7. locals.tf
  • 8. main.tf
  • 9. outputs.tf
  • 10. .gitignore
  • 11. 실행 방법
  • 12. 생성되는 리소스 흐름
  • 13. 초보자가 주의할 점
  • 14. 마무리

1. 이번 글에서 만들 구조

이번 글에서 만들 AWS 리소스는 다음과 같다.

VPC
Public Subnet
Internet Gateway
Route Table
Route Table Association
Security Group
EC2 Instance

구조를 간단히 표현하면 다음과 같다.

Internet
   ↓
Internet Gateway
   ↓
Public Subnet
   ↓
EC2 Instance

EC2에 SSH로 접속할 수 있도록 Security Group도 함께 만든다.

다만 보안상 실제 운영에서는 SSH를 모든 IP에 열어두면 안 된다. 이 글에서는 학습을 위해 변수로 허용 IP를 입력받도록 구성한다.


2. 프로젝트 폴더 구조

먼저 다음과 같은 폴더를 만든다.

terraform-aws-basic/
├── versions.tf
├── provider.tf
├── variables.tf
├── terraform.tfvars
├── locals.tf
├── main.tf
├── outputs.tf
└── .gitignore

각 파일의 역할은 다음과 같다.

파일 역할
versions.tf Terraform / Provider 버전 정의
provider.tf AWS Provider 설정
variables.tf 입력 변수 정의
terraform.tfvars 변수 값 입력
locals.tf 공통 태그 등 내부 값 정의
main.tf 실제 AWS 리소스 정의
outputs.tf 생성된 리소스의 주요 값 출력
.gitignore Git에 올리면 안 되는 파일 제외

3. versions.tf

versions.tf에는 Terraform 버전과 AWS Provider 버전을 정의한다.

terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

이 파일을 두는 이유는 프로젝트를 실행하는 환경마다 Terraform이나 Provider 버전이 달라지는 것을 방지하기 위해서다.

버전이 달라지면 같은 코드라도 동작 방식이 달라질 수 있다. 따라서 프로젝트에서는 가능한 버전을 명시하는 것이 좋다.


4. provider.tf

provider.tf에는 AWS Provider 설정을 작성한다.

provider "aws" {
  region = var.aws_region
}

여기서 var.aws_regionvariables.tf에서 정의하고, terraform.tfvars에서 실제 값을 넣을 예정이다.

주의할 점은 AWS Access Key를 코드에 직접 작성하지 않는 것이다.

# 권장하지 않음
provider "aws" {
  access_key = "..."
  secret_key = "..."
  region     = "ap-northeast-2"
}

로컬에서는 AWS CLI profile이나 환경 변수를 사용하는 것이 좋다.

aws configure

또는 환경 변수를 사용할 수도 있다.

export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_DEFAULT_REGION="ap-northeast-2"

운영 CI/CD에서는 Access Key를 직접 저장하기보다 OIDC 기반 Assume Role 방식을 사용하는 것이 더 안전하다.


5. variables.tf

variables.tf에는 이 프로젝트에서 사용할 입력 변수를 정의한다.

variable "aws_region" {
  description = "AWS region"
  type        = string
}

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

variable "vpc_cidr" {
  description = "VPC CIDR block"
  type        = string
}

variable "public_subnet_cidr" {
  description = "Public subnet CIDR block"
  type        = string
}

variable "availability_zone" {
  description = "Availability zone for public subnet"
  type        = string
}

variable "allowed_ssh_cidr" {
  description = "CIDR block allowed to access EC2 via SSH"
  type        = string
}

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

변수로 분리한 이유는 다음과 같다.

환경마다 값이 달라질 수 있다.
코드를 재사용하기 쉬워진다.
하드코딩을 줄일 수 있다.

예를 들어 개발 환경에서는 t3.micro를 쓰고, 테스트 환경에서는 다른 인스턴스 타입을 사용할 수 있다.


6. terraform.tfvars

terraform.tfvars에는 실제 변수 값을 입력한다.

aws_region         = "ap-northeast-2"
project_name       = "demo"

vpc_cidr           = "10.0.0.0/16"
public_subnet_cidr = "10.0.1.0/24"
availability_zone  = "ap-northeast-2a"

allowed_ssh_cidr   = "내_IP/32"

instance_type      = "t3.micro"

allowed_ssh_cidr에는 본인의 IP를 넣는 것이 좋다.

예를 들어 현재 공인 IP가 1.2.3.4라면 다음처럼 입력한다.

allowed_ssh_cidr = "1.2.3.4/32"

학습 중이라고 해도 SSH를 0.0.0.0/0으로 열어두는 것은 권장하지 않는다.

# 권장하지 않음
allowed_ssh_cidr = "0.0.0.0/0"

또한 terraform.tfvars에는 비밀번호, 토큰, Access Key 같은 민감정보를 넣지 않는 것이 좋다.


7. locals.tf

locals.tf에는 코드 내부에서 반복해서 사용할 값을 정의한다.

이번 예제에서는 공통 태그를 정의한다.

locals {
  common_tags = {
    Project   = var.project_name
    ManagedBy = "terraform"
  }
}

이렇게 해두면 여러 리소스에서 같은 태그를 반복해서 작성하지 않아도 된다.

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

variablelocals는 역할이 다르다.

구분 역할
variable 외부에서 입력받는 값
locals 코드 내부에서 계산하거나 재사용하는 값

8. main.tf

이제 실제 AWS 리소스를 정의한다.

이번 템플릿에서는 다음 리소스를 만든다.

VPC
Public Subnet
Internet Gateway
Route Table
Route Table Association
Security Group
AMI 조회
EC2 Instance

8.1 VPC

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

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

VPC는 AWS 네트워크의 기본이 되는 리소스다.

enable_dns_supportenable_dns_hostnames는 VPC 내부 DNS 사용과 관련된 설정이다. 초보 단계에서는 켜두는 것이 일반적으로 편하다.


8.2 Public Subnet

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidr
  availability_zone       = var.availability_zone
  map_public_ip_on_launch = true

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

Subnet은 VPC 안에 만들어지는 네트워크 구간이다.

여기서 중요한 부분은 vpc_id다.

vpc_id = aws_vpc.main.id

Subnet은 VPC가 있어야 생성 가능하다. 따라서 Terraform은 이 참조를 보고 VPC를 먼저 만든다.

map_public_ip_on_launch = true는 이 Subnet에서 생성되는 EC2에 public IP를 자동으로 할당하도록 하는 설정이다.


8.3 Internet Gateway

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

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

Internet Gateway는 VPC가 인터넷과 통신할 수 있도록 해주는 리소스다.

Public Subnet이 인터넷과 통신하려면 Internet Gateway가 필요하다.


8.4 Route Table

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

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

Route Table은 네트워크 트래픽이 어디로 가야 하는지 정의한다.

여기서는 모든 외부 트래픽을 Internet Gateway로 보내도록 설정한다.

0.0.0.0/0 → Internet Gateway

8.5 Route Table Association

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

Route Table을 만들었다고 해서 자동으로 Subnet에 적용되는 것은 아니다.

Subnet에 Route Table을 연결해야 한다.

Public Subnet
→ Public Route Table 연결

8.6 Security Group

resource "aws_security_group" "ec2" {
  name        = "${var.project_name}-ec2-sg"
  description = "Security group for EC2"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "SSH from allowed IP"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.allowed_ssh_cidr]
  }

  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    description = "Allow all outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

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

Security Group은 EC2에 대한 방화벽 역할을 한다.

이 예제에서는 SSH와 HTTP를 허용한다.

22번 포트 → 내 IP만 허용
80번 포트 → 전체 허용

실무에서는 SSH를 직접 여는 대신 SSM Session Manager를 사용하는 방식도 많이 사용한다.


8.7 AMI 조회

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

AMI ID를 직접 하드코딩할 수도 있지만, 여기서는 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 정보를 조회할 때 사용한다.


8.8 EC2 Instance

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]

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

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

이 EC2는 Public Subnet에 생성되고, Security Group을 통해 SSH와 HTTP 접근을 허용한다.

user_data는 EC2가 처음 생성될 때 실행되는 스크립트다.

위 예제에서는 Apache HTTP 서버를 설치하고, 간단한 HTML 파일을 생성한다.

http://EC2_PUBLIC_IP

브라우저에서 위 주소로 접속하면 Hello Terraform을 확인할 수 있다.


9. outputs.tf

outputs.tf는 Terraform이 만든 리소스의 주요 값을 외부로 꺼내는 파일이다.

단순히 apply 이후 화면에 출력하기 위한 용도만 있는 것은 아니다.

output은 Terraform 코드의 결과를 외부로 전달하는 인터페이스다.

이번 예제에서는 VPC ID, Subnet ID, EC2 Public IP, EC2 접속 URL을 출력한다.

output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "public_subnet_id" {
  description = "Public subnet ID"
  value       = aws_subnet.public.id
}

output "ec2_public_ip" {
  description = "EC2 public IP"
  value       = aws_instance.app.public_ip
}

output "ec2_public_dns" {
  description = "EC2 public DNS"
  value       = aws_instance.app.public_dns
}

output "web_url" {
  description = "Web URL"
  value       = "http://${aws_instance.app.public_ip}"
}

apply 이후 다음 명령어로 output을 확인할 수 있다.

terraform output

특정 값만 가져오고 싶다면 다음처럼 사용할 수 있다.

terraform output -raw web_url

output은 이후 CI/CD에서도 활용할 수 있다.

WEB_URL=$(terraform output -raw web_url)

curl -f "$WEB_URL"

즉, output은 Terraform으로 만든 값을 다음 단계로 넘기는 통로다.


10. .gitignore

Terraform 프로젝트에서는 Git에 올리면 안 되는 파일이 있다.

대표적으로 state 파일과 .terraform 디렉토리는 제외해야 한다.

.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl

terraform.tfvars
*.tfvars

다만 .terraform.lock.hcl은 Provider 버전 고정을 위해 Git에 포함하는 경우도 많다.

초보 단계에서는 다음처럼 이해하면 된다.

반드시 제외:
- .terraform/
- *.tfstate
- *.tfstate.backup

상황에 따라 관리:
- .terraform.lock.hcl
- terraform.tfvars

terraform.tfvars에 민감정보가 들어간다면 반드시 Git에서 제외해야 한다.

예제용 값을 공유하고 싶다면 다음처럼 샘플 파일을 따로 만들 수 있다.

terraform.tfvars.example

11. 실행 방법

이제 실제로 실행해보자.


11.1 초기화

terraform init

Provider를 다운로드하고 Terraform 프로젝트를 초기화한다.


11.2 코드 포맷

terraform fmt

Terraform 코드 스타일을 정리한다.


11.3 문법 검사

terraform validate

Terraform 코드가 문법적으로 올바른지 검사한다.


11.4 변경 계획 확인

terraform plan

어떤 리소스가 생성될지 확인한다.

초보자일수록 plan 결과를 천천히 읽어보는 것이 좋다.


11.5 리소스 생성

terraform apply

확인 메시지가 나오면 yes를 입력한다.

생성이 완료되면 output 값이 출력된다.

web_url = "http://..."

브라우저에서 해당 URL에 접속하면 Hello Terraform을 확인할 수 있다.


11.6 리소스 삭제

실습이 끝났다면 비용이 발생하지 않도록 리소스를 삭제한다.

terraform destroy

EC2는 실행 중이면 비용이 발생할 수 있으므로 실습 후 반드시 정리하는 것이 좋다.


12. 생성되는 리소스 흐름

이번 템플릿에서 Terraform이 생성하는 의존성 흐름은 다음과 같다.

 

 

Terraform은 코드 순서가 아니라 참조 관계를 기반으로 이 순서를 계산한다.

Terraform에서 중요한 것은 파일에 적힌 순서가 아니라 리소스 간 참조 관계다.

13. 초보자가 주의할 점

13.1 SSH를 전체 공개하지 않기

SSH를 0.0.0.0/0으로 열면 누구나 접속을 시도할 수 있다.

# 권장하지 않음
allowed_ssh_cidr = "0.0.0.0/0"

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

allowed_ssh_cidr = "1.2.3.4/32"

13.2 State 파일을 Git에 올리지 않기

State에는 리소스 ID와 속성 정보가 들어간다. 경우에 따라 민감한 값이 포함될 수도 있다.

따라서 state 파일은 Git에 올리면 안 된다.

*.tfstate
*.tfstate.backup

13.3 Access Key를 코드에 넣지 않기

AWS Access Key와 Secret Key를 Terraform 코드에 직접 작성하지 않는다.

로컬에서는 AWS CLI profile을 사용하고, CI/CD에서는 OIDC 기반 Assume Role을 사용하는 것이 좋다.


13.4 실습 후 destroy 하기

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

실습이 끝났다면 반드시 삭제한다.

terraform destroy

13.5 이 템플릿은 운영용이 아니다

이 글의 템플릿은 학습용 기본 구조다.

운영 환경에서는 다음 요소들을 추가로 고려해야 한다.

Private Subnet
NAT Gateway
Bastion 또는 SSM
ALB
HTTPS
CloudWatch Logs
IAM 최소 권한
S3 backend
State locking
환경 분리
모듈화

14. 마무리

이번 글에서는 Terraform 초보자가 바로 실행해볼 수 있는 AWS 기본 프로젝트 템플릿을 만들어보았다.

구성한 리소스는 다음과 같다.

VPC
Public Subnet
Internet Gateway
Route Table
Security Group
EC2

이 템플릿을 통해 다음 내용을 익힐 수 있다.

Terraform 파일 구조
변수 사용
output 사용
리소스 간 참조
Terraform 실행 흐름
기본 AWS 네트워크 구조

초보자라면 이 구조를 직접 실행해보고, AWS 콘솔에서 실제로 어떤 리소스가 생성되었는지 확인해보는 것이 좋다.


한 줄 정리

Terraform은 작은 템플릿을 직접 실행해보면서 익히는 것이 가장 빠르다.


다음 글에서는 이 템플릿의 기반이 되는 VPC, Subnet, Route Table 구조를 더 자세히 정리해본다.