Skip to content
ioob.dev
Go back

Terraform 2편 — 설치와 첫 배포

· 9분 읽기
Terraform 시리즈 (2/15)
  1. Terraform 1편 — Terraform이란
  2. Terraform 2편 — 설치와 첫 배포
  3. Terraform 3편 — HCL 문법
  4. Terraform 4편 — 변수와 출력
  5. Terraform 5편 — 프로바이더
  6. Terraform 6편 — 리소스와 의존성
  7. Terraform 7편 — 데이터 소스와 Import
  8. Terraform 8편 — State 관리
  9. Terraform 9편 — 모듈
  10. Terraform 10편 — 반복과 조건
  11. Terraform 11편 — 워크스페이스와 환경 분리
  12. Terraform 12편 — Kubernetes와 Helm 프로바이더
  13. Terraform 13편 — CI/CD 통합
  14. Terraform 14편 — 테스트와 정책
  15. Terraform 15편 — 실전 패턴과 함정
Table of contents

Table of contents

설치부터 첫 배포까지 한 번에

이론은 1편에서 충분히 다뤘다. 이번 편은 손을 움직여서 Terraform을 체감하는 게 목적이다. 설치하고, AWS와 연결하고, .tf 파일 하나로 EC2 인스턴스를 띄운 뒤 다시 지운다. 네 가지 명령(init, plan, apply, destroy)의 의미를 직접 눈으로 확인한다.

AWS 프리 티어 계정이 있다는 가정이다. 없다면 실습 부분은 눈으로만 따라와도 된다. 개념은 같다.

Terraform 설치

Terraform은 Go로 작성된 단일 바이너리다. 설치는 복잡하지 않다. 운영체제별로 공식 방법을 고르면 된다.

macOS — Homebrew

brew tap hashicorp/tap
brew install hashicorp/tap/terraform

HashiCorp 공식 tap을 추가하고 설치한다. brew install terraform만 써도 동작하지만, 공식 tap을 쓰는 편이 버전 관리가 확실하다.

Linux — 패키지 매니저 (Ubuntu/Debian 예시)

# HashiCorp GPG 키 등록
wget -O- https://apt.releases.hashicorp.com/gpg | \
  sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

# apt 저장소 추가
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
  https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/hashicorp.list

sudo apt update && sudo apt install terraform

GPG 키와 저장소를 등록한 뒤 apt로 설치한다. CentOS/Fedora는 yum 저장소 방식으로 비슷하게 설치한다.

Windows — Chocolatey

choco install terraform

한 줄로 끝난다. WSL을 쓴다면 위의 Linux 방식도 가능하다.

버전 확인

설치가 끝났는지 바로 확인한다.

terraform version

출력이 이렇게 나오면 성공이다.

Terraform v1.9.5
on darwin_arm64

tfenv로 여러 버전 관리

실무에선 프로젝트마다 Terraform 버전이 다를 수 있다. tfenv를 쓰면 프로젝트별로 쉽게 전환된다. 필수는 아니지만 알아두면 편하다.

# macOS
brew install tfenv

# 특정 버전 설치하고 사용
tfenv install 1.9.5
tfenv use 1.9.5

# 디렉토리마다 버전 고정 (.terraform-version 파일)
echo "1.9.5" > .terraform-version

.terraform-version 파일이 있으면 해당 디렉토리에 들어설 때 자동으로 그 버전으로 전환된다. Ruby의 rbenv, Node의 nvm과 같은 발상이다.

AWS 계정과 CLI 준비

Terraform이 AWS에 뭔가를 만들려면 자격 증명이 필요하다. Access Key를 발급받고 AWS CLI 프로파일을 설정해둔다.

IAM 사용자 생성과 Access Key 발급

콘솔에서 IAM → Users → Create user → Attach policies → (실습용이면 AdministratorAccess, 실무에선 최소 권한). 만들어진 사용자에서 “Security credentials” 탭으로 이동해 “Create access key”를 누른다. Use case는 “Command Line Interface (CLI)“를 고른다.

발급된 Access Key ID와 Secret Access Key는 한 번만 보여준다. 꼭 안전한 곳에 저장한다. 커밋 실수로 유출되면 악몽이 시작된다.

AWS CLI 설치와 프로파일 설정

# macOS
brew install awscli

# 프로파일 설정
aws configure --profile terraform-demo

aws configure를 실행하면 네 가지를 물어본다.

AWS Access Key ID [None]: AKIA....
AWS Secret Access Key [None]: ....
Default region name [None]: ap-northeast-2
Default output format [None]: json

이렇게 입력한 정보는 ~/.aws/credentials~/.aws/config에 저장된다. 여러 프로파일을 둘 수 있다.

확인

aws sts get-caller-identity --profile terraform-demo

출력에 계정 ID와 사용자 ARN이 찍히면 준비 완료다. 여기서 에러가 나면 키가 잘못됐거나 네트워크 문제니 먼저 해결한다.

프로젝트 구조 잡기

Terraform은 디렉토리 단위로 동작한다. 해당 디렉토리의 모든 .tf 파일을 읽어들여 하나의 설정으로 취급한다. 실습용 폴더를 하나 만든다.

mkdir terraform-first-deploy && cd terraform-first-deploy

폴더 안은 비어있다. 여기에 파일을 하나씩 추가한다.

provider 블록 — AWS와 연결

가장 먼저 “어떤 프로바이더를 쓸지”부터 선언한다. providers.tf 파일을 만든다.

# providers.tf
terraform {
  required_version = ">= 1.5.0"

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

provider "aws" {
  region  = "ap-northeast-2"
  profile = "terraform-demo"
}

세 가지를 선언했다.

프로바이더 세부 이야기는 5편에서 파고든다. 지금은 “AWS와 연결하는 선언” 정도로 받아들이면 된다.

첫 리소스 — EC2와 S3

main.tf를 만들어서 리소스를 두 개 선언한다. 프리 티어에 들어가는 t3.micro EC2 하나와 S3 버킷 하나다.

# main.tf

# Amazon Linux 2023 AMI를 자동으로 찾아온다
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

# EC2 인스턴스
resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.micro"

  tags = {
    Name        = "terraform-first-deploy"
    ManagedBy   = "terraform"
    Environment = "demo"
  }
}

# S3 버킷 (이름은 전 세계적으로 유일해야 해서 랜덤 접미사 추가)
resource "random_id" "bucket_suffix" {
  byte_length = 4
}

resource "aws_s3_bucket" "demo" {
  bucket = "terraform-first-deploy-${random_id.bucket_suffix.hex}"

  tags = {
    ManagedBy = "terraform"
  }
}

data "aws_ami"는 AMI를 코드로 하드코딩하지 않고 “Amazon이 게시한 가장 최신 Amazon Linux 2023”을 런타임에 찾아오게 한다. 이런 데이터 소스 활용은 7편에서 자세히 다룬다.

S3 버킷 이름은 전 세계적으로 유일해야 한다. 그래서 random_id 리소스로 4바이트짜리 hex 접미사를 붙였다. random 프로바이더도 required_providers에 추가해야 한다.

# providers.tf에 추가
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }
}

4대 명령어의 의미

이제 가장 중요한 순간이다. Terraform의 핵심 워크플로우 네 단계를 차례로 실행한다.

flowchart LR
    INIT[terraform init<br/>프로바이더 다운로드] --> PLAN[terraform plan<br/>변경점 미리보기]
    PLAN --> APPLY[terraform apply<br/>실제 적용]
    APPLY --> USE[인프라 운영]
    USE --> DESTROY[terraform destroy<br/>전부 삭제]
    USE -.->|.tf 수정 후| PLAN

terraform init

프로젝트를 처음 시작하거나 프로바이더가 바뀌었을 때 실행한다.

terraform init

이 명령은 두 가지를 한다.

  1. required_providers에 쓴 프로바이더를 Terraform Registry에서 내려받는다 (~/.terraform.d/plugin-cache 또는 프로젝트 내 .terraform/ 에 저장됨)
  2. State 저장소(백엔드)를 초기화한다. 기본값은 로컬 파일

결과로 .terraform/ 디렉토리와 .terraform.lock.hcl 파일이 생긴다. lock 파일은 “이 프로바이더 버전으로 고정”을 기록하는 역할이다. Git에 커밋해둔다.

terraform plan

이 명령이 Terraform의 가장 중요한 안전장치다.

terraform plan

.tf에 적힌 바라는 상태와 현재 State를 비교해서 “어떤 리소스가 생기고, 바뀌고, 사라질지”를 보여준다. 실제로는 아무것도 바꾸지 않는다. 출력은 대략 이런 식이다.

Terraform will perform the following actions:

  # aws_instance.web will be created
  + resource "aws_instance" "web" {
      + ami                          = "ami-0c..."
      + instance_type                = "t3.micro"
      + ...
    }

  # aws_s3_bucket.demo will be created
  + resource "aws_s3_bucket" "demo" {
      + bucket = "terraform-first-deploy-a1b2c3d4"
      + ...
    }

Plan: 3 to add, 0 to change, 0 to destroy.

+는 새로 만든다는 의미, -는 삭제, ~는 수정, -/+는 재생성(기존 삭제 후 새로 만듦)이다. apply 전에 이 출력을 꼭 눈으로 확인하는 습관을 들여야 한다. 특히 -/+가 보이면 무중단이 깨지는 변경이라 위험하다.

terraform apply

plan에서 본 변경점을 실제로 적용한다.

terraform apply

실행하면 먼저 plan을 다시 한 번 보여주고, “yes”를 입력해야 진행한다. 자동화 환경에선 -auto-approve 플래그로 넘길 수 있지만, 손으로 할 땐 꼭 확인하고 입력한다.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_s3_bucket.demo: Creating...
aws_instance.web: Creating...
aws_s3_bucket.demo: Creation complete after 2s
aws_instance.web: Still creating... [10s elapsed]
aws_instance.web: Creation complete after 23s [id=i-0abc...]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

이 시점에 AWS 콘솔을 열어보면 실제로 인스턴스와 버킷이 만들어진 걸 확인할 수 있다. terraform.tfstate 파일도 생겼는데, Terraform이 “내가 만든 것들”을 기록한 파일이다. 절대 손으로 수정하지 않고, Git에도 올리지 않는다 (원칙적으로는 원격 백엔드에 저장하는 게 맞다. 이 얘기는 다음 편 이후에 깊게 다룬다).

terraform destroy

실습이 끝났으면 리소스를 깨끗이 지운다.

terraform destroy

plan과 반대로 모든 리소스를 “삭제” 상태로 계획한 뒤, yes를 받으면 실제로 지운다.

Plan: 0 to add, 0 to change, 3 to destroy.
...
Destroy complete! Resources: 3 destroyed.

실습이 끝났으면 꼭 destroy를 돌려둔다. 프리 티어 범위를 넘는 리소스를 방치하면 다음 달 청구서가 반갑지 않을 수 있다.

한 번 더 apply 해보기 — 멱등성 체험

선언형의 핵심인 멱등성을 체감할 수 있는 간단한 실험이다. apply로 리소스를 만든 뒤, 아무것도 바꾸지 말고 다시 apply를 돌려본다.

terraform apply

결과가 이렇게 나온다.

No changes. Your infrastructure matches the configuration.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

아무것도 하지 않는다. 이게 멱등성이다. “선언한 상태”와 “현재 상태”가 같으면 Terraform은 조용히 지나간다. 명령형 스크립트였다면 “이미 존재합니다” 같은 에러를 뱉었을 것이다.

이번에는 .tf의 태그를 하나 바꿔본다.

tags = {
  Name        = "terraform-first-deploy"
  ManagedBy   = "terraform"
  Environment = "staging"   # demo → staging
}

다시 plan을 돌리면 차이가 딱 잡힌다.

~ tags = {
    "Environment" = "demo" -> "staging"
    ...
  }

Plan: 0 to add, 1 to change, 0 to destroy.

딱 바뀐 만큼만 바꾼다. 이게 Terraform의 힘이다.

결과물을 눈으로 — output 블록

만든 리소스의 정보를 콘솔에 찍어주고 싶을 때가 있다. outputs.tf를 만든다.

# outputs.tf
output "instance_id" {
  description = "EC2 인스턴스 ID"
  value       = aws_instance.web.id
}

output "instance_public_ip" {
  description = "EC2 퍼블릭 IP"
  value       = aws_instance.web.public_ip
}

output "s3_bucket_name" {
  description = "S3 버킷 이름"
  value       = aws_s3_bucket.demo.bucket
}

terraform apply 마지막에 이 값들이 출력된다. 또는 언제든 terraform output으로 조회할 수 있다.

terraform output instance_public_ip
# "3.35.xxx.xxx"

output은 모듈 간 값 전달, CI/CD 스크립트 연동 등에 두루 쓰인다. 4편에서 깊게 다룬다.

저장소에 올릴 때 주의할 것 — .gitignore

이 프로젝트를 Git에 커밋한다면 .gitignore가 필수다. 아래를 포함해둔다.

# Terraform
.terraform/
.terraform.lock.hcl   # 프로젝트에 따라 커밋하기도 함
*.tfstate
*.tfstate.*
*.tfplan
crash.log
.env
terraform.tfvars      # 민감한 변수 값
*.auto.tfvars

핵심은 두 가지다.

State를 팀과 공유하려면 로컬 파일 대신 S3 + DynamoDB, Terraform Cloud, GitLab 같은 원격 백엔드를 쓴다. 이 주제는 따로 한 편이 필요할 정도라 이 시리즈의 후속 글에서 다루거나, HashiCorp 공식 문서를 참고한다.

.terraform.lock.hcl은 프로바이더 버전을 고정하는 파일이라 커밋하는 편이 일반적이다. 팀원 간 버전이 달라져서 생기는 이슈를 막아준다.

워크플로우 리뷰

지금까지의 흐름을 다시 한 번 그려보면 이렇다.

sequenceDiagram
    participant Dev as 개발자
    participant TF as Terraform
    participant State as tfstate
    participant AWS as AWS API

    Dev->>TF: terraform init
    TF->>TF: 프로바이더 다운로드
    Dev->>TF: terraform plan
    TF->>State: 현재 상태 조회
    TF->>AWS: 리소스 현황 질의
    TF->>Dev: 변경점 출력
    Dev->>TF: terraform apply (yes)
    TF->>AWS: 리소스 생성/수정/삭제
    TF->>State: 결과 기록
    TF->>Dev: 완료 메시지
    Note over Dev,AWS: 인프라 운영
    Dev->>TF: terraform destroy
    TF->>AWS: 전부 삭제
    TF->>State: State 정리

이 네 단계는 어떤 규모의 프로젝트에서도 그대로 통한다. 달라지는 건 State를 어디에 저장할지, 프로바이더를 몇 개 쓸지, CI에서 plan을 어떻게 자동화할지 정도다.

다음으로

첫 배포까지 해봤으니 이제 Terraform을 “쓸” 준비는 된 셈이다. 이 시점에서 코드를 더 잘 쓰기 위해 필요한 건 언어 자체에 대한 이해다. HCL이 어떤 구조로 동작하는지, 어떤 표현식이 가능한지를 알아야 실제 프로젝트에서 가독성 좋은 코드를 쓸 수 있다.

다음 편에서는 HCL 문법을 본격적으로 파고든다. 블록/인자/표현식의 구조부터 조건식, for 표현식, 내장 함수까지 훑는다.


3편: HCL 문법


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Terraform 1편 — Terraform이란
Next Post
Terraform 3편 — HCL 문법