Terraform ์ต์ ๊ธฐ์ค ์ด๋ณด์ ์๋ฒฝ ๊ฐ์ด๋: ์์คํ
์๊ตฌ์ฌ์, HCL ๋ฌธ๋ฒ ๊ธฐ์ด, init/plan/apply/destroy 4๋ ๋ผ์ดํ์ฌ์ดํด, ๋ณ์(Variables)ยท๋ก์ปฌ(Locals)ยท์ถ๋ ฅ(Outputs), ์ํ(State) ๊ด๋ฆฌ ๋ฐ S3+DynamoDB ์๊ฒฉ ๋ฐฑ์๋, ์ฌ์ฌ์ฉ ๋ชจ๋(Module) ๋ฐ AWS/Docker ์ค์ ์ธํ๋ผ ํ๋ก๋น์ ๋
## 1. ํ์ ์ฌ์ ์ค์น ์๊ฑด & ์์คํ
์๊ตฌ์ฌ์ (Prerequisites & System Requirements)
**Terraform**์ HashiCorp์์ ๊ฐ๋ฐํ ์ธ๊ณ ํ์ค **์ฝ๋ํ ์ธํ๋ผ(IaC, Infrastructure as Code)** ๋๊ตฌ์
๋๋ค. AWS, Azure, GCP, Kubernetes, Docker ๋ฑ ๋ค์ํ ํด๋ผ์ฐ๋ ๋ฐ ์จํ๋ ๋ฏธ์ค ์ธํ๋ผ ๋ฆฌ์์ค๋ฅผ HCL(HashiCorp Configuration Language) ์ฝ๋๋ก ์ ์ธํ๊ณ , ๋ฒ์ ๊ด๋ฆฌ ๋ฐ ์๋ ๋ฐฐํฌํ ์ ์์ต๋๋ค.
์ด๋ณด์๋ ํด๋ผ์ฐ๋ ๋น์ฉ ์์ด ๋ก์ปฌ์์ ์์ํ ์ ์๋๋ก ํ์ ์ฌ์ ํ๊ฒฝ ๊ตฌ์ฑ๋ถํฐ ์์ํฉ๋๋ค.
### ๐ป ์์คํ
์ค์น ๋ฐ ์คํ ์๊ตฌ์ฌ์ (System Requirements)
| ํญ๋ชฉ | ์ต์ ์ฌ์ (Minimum) | ๊ถ์ฅ ์ฌ์ (Recommended) | ์ฐธ๊ณ ๋ฐ ๋น๊ณ |
| :--- | :--- | :--- | :--- |
| **์ด์์ฒด์ (OS)** | Linux 64-bit (Ubuntu 20.04+, RHEL 8+), macOS 12+, Windows 10/11 64-bit | Linux (Ubuntu 22.04 LTS+) ๋๋ macOS (Apple Silicon) | POSIX ํธํ 64-bit OS ๊ถ์ฅ |
| **CPU** | 1 ์ฝ์ด ์ด์ (x86_64, ARM64) | 2 ์ฝ์ด ~ 4 ์ฝ์ด ์ด์ | ๋๊ท๋ชจ ์ธํ๋ผ DAG ์์กด์ฑ ๊ทธ๋ํ ๊ณ์ฐ ๋ฐ ๋ณ๋ ฌ API ํธ์ถ |
| **๋ฉ๋ชจ๋ฆฌ (RAM)** | 1 GB ์ด์ | 2 GB ~ 4 GB ์ด์ | ์๋ฐฑ ๊ฐ ๋ฆฌ์์ค ์ํ(State) ๊ฐฑ์ ๋ฐ Plan Diff ๋น๊ต ์ฐ์ฐ |
| **๋์คํฌ (Storage)** | 500 MB ์ด์์ ์ฌ์ ๊ณต๊ฐ | 5 GB ~ 10 GB ์ด์์ ๊ณ ์ SSD | ํ๋ก๋ฐ์ด๋ ํ๋ฌ๊ทธ์ธ ์บ์(`.terraform/providers`) ๋ฐ ๋ชจ๋ ์ ์ฅ์ |
| **๋คํธ์ํฌ / ๋ฐํ์** | Terraform 1.9+ ๋๋ OpenTofu 1.8+ CLI ๋จ์ผ ๋ฐ์ด๋๋ฆฌ | tfenv (๋ค์ค ๋ฒ์ ๊ด๋ฆฌ์) ๊ถ์ฅ | ์์๋ฐ์ด๋ HTTPS (Terraform Registry ๋ฐ ํด๋ผ์ฐ๋ API 443 ํฌํธ) |
---
### ๐ฆ OS๋ณ Terraform CLI ์ค์น ๋ฐฉ๋ฒ
Terraform์ ๋จ์ผ ์คํ ๋ฐ์ด๋๋ฆฌ๋ก ๋ฐฐํฌ๋๋ฏ๋ก ์ค์น๊ฐ ๋งค์ฐ ๊ฐํธํฉ๋๋ค:
```bash
# ๐ [1] macOS (Homebrew)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# ๐ง [2] Linux (Ubuntu / Debian)
sudo apt-get update && sudo apt-get install -y gnupg software-properties-common curl
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
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-get update && sudo apt-get install -y terraform
# ๐ช [3] Windows (PowerShell - winget ๋๋ choco)
winget install Hashicorp.Terraform
# ๋๋: choco install terraform
# โก [4] ๋ค์ค ๋ฒ์ ๊ด๋ฆฌ ๋๊ตฌ tfenv ์ฌ์ฉ ์ (๊ฐ๋ ฅ ์ถ์ฒ)
# ํ๋ก์ ํธ๋ณ๋ก Terraform ๋ฒ์ ์ ๋ค๋ฅด๊ฒ ๊ด๋ฆฌํ ๋ ํ์
git clone --depth=1 https://github.com/tfutils/tfenv.git ~/.tfenv
echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
tfenv install 1.9.5
tfenv use 1.9.5
# ์ค์น ํ์ธ
terraform -version
```
---
### ๐ ํด๋ผ์ฐ๋ ๊ณต๊ธ์(AWS) ์ธ์ฆ ํ๊ฒฝ ์ค์
AWS ์ธํ๋ผ๋ฅผ ํ๋ก๋น์ ๋ํ ๊ฒฝ์ฐ, CLI ํ๊ฒฝ์์ ์ธ์ฆ ํ ํฐ์ ์ฝ์ ์ ์๋๋ก ์ค์ ํฉ๋๋ค:
```bash
# ๋ฐฉ๋ฒ 1: AWS CLI ๊ณต์ ์ค์ ๋๊ตฌ
aws configure
# AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
# AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Default region name [None]: ap-northeast-2
# Default output format [None]: json
# ๋ฐฉ๋ฒ 2: ํฐ๋ฏธ๋ ํ๊ฒฝ ๋ณ์๋ก ์ง์ ์ฃผ์
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="ap-northeast-2"
```
---
## 2. ์ง๊ด์ ์ธ ๋ฉํ ๋ชจ๋ธ (Mental Model & Architecture)
Terraform์ **์ํ๋ ์ธํ๋ผ์ ์ต์ข
์ํ(Desired State)**๋ฅผ ์ฝ๋๋ก ์ ์ํ๋ฉด, **ํ์ฌ ์ธํ๋ผ์ ์ค์ ์ํ(Current State)**์ ๋น๊ตํ์ฌ ์ฐจ์ด์ ๋ง ์๋์ผ๋ก ๊ณ์ฐํด ์ ์ฉํ๋ **์ ์ธ์ (Declarative) ์์ง**์
๋๋ค.
```text
[Terraform ์ํฌํ๋ก์ฐ ๋ฉํ ๋ชจ๋ธ]
โโโโโโโโโโโโโโโโโโโ
โ main.tf (HCL) โ โโ(์์ฑ)โโโบ "์ด๋ค ์ธํ๋ผ๋ฅผ ๋ง๋ค ๊ฒ์ธ๊ฐ?" (์ ์ธ์ ์ฝ๋)
โโโโโโโโโโฌโโโโโโโโโ
โ
โผ 1. terraform init
โโโโโโโโโโโโโโโโโโโ
โ .terraform/ โ โโ(๋ค์ด๋ก๋)โโโบ AWS/Docker Provider ํ๋ฌ๊ทธ์ธ & ๋ชจ๋ ์ด๊ธฐํ
โโโโโโโโโโฌโโโโโโโโโ
โ
โผ 2. terraform plan
โโโโโโโโโโโโโโโโโโโ
โ Plan Diff Reportโ โโ(๋น๊ต)โโโบ Desired State (์ฝ๋) vs Current State (์ค์ ํด๋ผ์ฐ๋)
โโโโโโโโโโฌโโโโโโโโโ ๊ฒฐ๊ณผ: +2 to add, ~1 to change, -0 to destroy
โ
โผ 3. terraform apply
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Cloud Resources โ โโ(ํธ์ถ)โโโบ โ AWS VPC, EC2, S3, Docker Container ์์ฑ โ
โโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ 4. ์ํ ์ ์ฅ
โโโโโโโโโโโโโโโโโโโ
โ terraform. โ โโ(๋๊ธฐํ)โโโบ ์ค์ ํ๋ก๋น์ ๋๋ ๋ฆฌ์์ค์ ID, IP, ์์ฑ์
โ tfstate โ JSON ํ์์ ๋จ์ผ ์ง์ค ๊ณต๊ธ์(Single Source of Truth)์ผ๋ก ๋ณด๊ด
โโโโโโโโโโโโโโโโโโโ
```
### ๐ Terraform ํต์ฌ 4๋ ๊ตฌ์ฑ์์
1. **Provider (๊ณต๊ธ์)**: AWS, Azure, GCP, Docker, Kubernetes ๋ฑ ๋์ ํ๋ซํผ์ API์ ํต์ ํ๋ ํ๋ฌ๊ทธ์ธ.
2. **Resource (๋ฆฌ์์ค)**: VPC, EC2, S3 ๋ฒํท, ๋์ปค ์ปจํ
์ด๋ ๋ฑ ์ค์ ๋ก ์์ฑํ๊ณ ๊ด๋ฆฌํ ์ธํ๋ผ ๊ฐ์ฒด.
3. **State (์ํ ํ์ผ)**: ์ฝ๋์ ์ค์ ํด๋ผ์ฐ๋ ์ธํ๋ผ ๋ฆฌ์์ค๋ฅผ 1:1๋ก ๋งคํํด๋๋ JSON ๋ฉํ๋ฐ์ดํฐ (`terraform.tfstate`).
4. **Data Source (๋ฐ์ดํฐ ์์ค)**: ์ด๋ฏธ ํด๋ผ์ฐ๋์ ์กด์ฌํ๋ ๋ฆฌ์์ค(์: ์ต์ Ubuntu AMI ID, ๊ธฐ์กด VPC ID)๋ฅผ ์ฝ์ด์ ์ฝ๋์์ ์ฐธ์กฐ.
---
## 3. [์ด๋ณด์ 1๋จ๊ณ] ํฌ๋ก์๋: ๋ก์ปฌ ํ์ผ ํ๋ก๋ฐ์ด๋๋ก ๋ฌด๊ณผ๊ธ ์ฆ์ ์ค์ต
ํด๋ผ์ฐ๋ ๊ณ์ ์ด๋ ๊ฒฐ์ ์นด๋ ๋ฑ๋ก ์์ด ๋ด ์ปดํจํฐ ๋ก์ปฌ ํ๊ฒฝ์์ Terraform์ ์ ์ฒด ๋ผ์ดํ์ฌ์ดํด(`init โ plan โ apply โ destroy`)์ ์ฆ์ ์ค์ตํ ์ ์๋ ์ต์ ๋์ ์์ ์
๋๋ค.
### ๐ ์์ ํ ์คํ ์ฝ๋ (`main.tf`)
์์
์ฉ ๋น ํด๋(`terraform-quickstart/`)๋ฅผ ์์ฑํ๊ณ ์๋ `main.tf` ํ์ผ์ ์์ฑํฉ๋๋ค:
```hcl
# 1. ํ
๋ผํผ ์ค์ ๋ธ๋ก ๋ฐ ์๊ตฌ ํ๋ก๋ฐ์ด๋ ๋ช
์
terraform {
required_version = ">= 1.5.0"
required_providers {
# ๋ก์ปฌ ํ์ผ ์์คํ
์ ์กฐ์ํ๋ ๊ณต์ local ํ๋ก๋ฐ์ด๋
local = {
source = "hashicorp/local"
version = "~> 2.5.0"
}
}
}
# 2. ๋ก์ปฌ ํ์ผ ๋ฆฌ์์ค ์ ์
resource "local_file" "hello_world" {
filename = "${path.module}/lucky_output.txt"
content = "์๋
ํ์ธ์! LuckyTechWorld Terraform ํต์คํํธ ์ค์ต ํ์ผ์
๋๋ค.\n์์ฑ ์๊ฐ: 2026-10-09\n"
# ํ์ผ ๊ถํ ์ค์ (์ฝ๊ธฐ/์ฐ๊ธฐ)
file_permission = "0644"
}
# 3. ์์ฑ ๊ฒฐ๊ณผ ์ถ๋ ฅ (Output)
output "created_file_name" {
description = "์ฑ๊ณต์ ์ผ๋ก ์์ฑ๋ ํ์ผ์ ๊ฒฝ๋ก"
value = local_file.hello_world.filename
}
```
### ๐ป ํฐ๋ฏธ๋ ๋ช
๋ น์ด ์คํ ๋จ๊ณ
```bash
# 1. ํ๋ก๋ฐ์ด๋ ํ๋ฌ๊ทธ์ธ ์ด๊ธฐํ ๋ฐ ๋ค์ด๋ก๋
terraform init
# 2. ๋ณ๊ฒฝ ๊ณํ ํ์ธ (Dry-run ์คํ ๊ณํ ๊ฒํ )
terraform plan
# 3. ์ค์ ๋ฆฌ์์ค ํ๋ก๋น์ ๋ (์๋ ์น์ธ ์ต์
: -auto-approve)
terraform apply -auto-approve
# 4. ์์ฑ๋ ํ์ผ ํ์ธ
cat lucky_output.txt
# 5. ๋ฆฌ์์ค ์ ๋ฆฌ ๋ฐ ์ญ์
terraform destroy -auto-approve
```
### ๐ฅ๏ธ ์์ ์ฝ์ ์ถ๋ ฅ ๊ฒฐ๊ณผ
```text
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/local versions matching "~> 2.5.0"...
- Installing hashicorp/local v2.5.1...
- Installed hashicorp/local v2.5.1 (signed by HashiCorp)
Terraform has been successfully initialized!
$ terraform apply -auto-approve
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# local_file.hello_world will be created
+ resource "local_file" "hello_world" {
+ content = "์๋
ํ์ธ์! LuckyTechWorld Terraform ํต์คํํธ ์ค์ต ํ์ผ์
๋๋ค.\n์์ฑ ์๊ฐ: 2026-10-09\n"
+ file_permission = "0644"
+ filename = "./lucky_output.txt"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ created_file_name = "./lucky_output.txt"
local_file.hello_world: Creating...
local_file.hello_world: Creation complete after 0s [id=3b98e...]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
created_file_name = "./lucky_output.txt"
```
> **ํฌ์ธํธ**: ์คํ ํ ๋๋ ํ ๋ฆฌ์ `.terraform/` (ํ๋ก๋ฐ์ด๋ ํ๋ฌ๊ทธ์ธ ์บ์), `.terraform.lock.hcl` (์์กด์ฑ ์ ๊ธ ํ์ผ), ๊ทธ๋ฆฌ๊ณ `terraform.tfstate` (์ธํ๋ผ ์ํ ํ์ผ)์ด ์์ฑ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
---
## 4. [2๋จ๊ณ] HCL ํต์ฌ ๋ฌธ๋ฒ: ๋ณ์(Variables), ๋ก์ปฌ(Locals), ์ถ๋ ฅ(Outputs) & ์์กด์ฑ
ํ๋์ฝ๋ฉ์ ํผํ๊ณ ํ๊ฒฝ๋ณ(๊ฐ๋ฐ/์ด์)๋ก ์ ์ฐํ๊ฒ ์ธํ๋ผ๋ฅผ ์ฌ์ฌ์ฉํ๊ธฐ ์ํด **์
๋ ฅ ๋ณ์(Variables)**, **์ค๊ฐ ๊ณ์ฐ๊ฐ(Locals)**, **์ถ๋ ฅ๊ฐ(Outputs)**์ ๋ถ๋ฆฌํฉ๋๋ค.
### ๐ ํ์ผ ๋ถ๋ฆฌ ๋ชจ๋ฒ ๊ตฌ์กฐ
```text
โโโ main.tf # ๋ฉ์ธ ๋ฆฌ์์ค ์ ์ธ
โโโ variables.tf # ์
๋ ฅ ๋ณ์ ํ์
๋ฐ ๊ธฐ๋ณธ๊ฐ ์ ์ธ
โโโ terraform.tfvars # ์ค์ ์ฃผ์
ํ ๋ณ์ ๊ฐ (Git ์ปค๋ฐ ์ฃผ์)
โโโ outputs.tf # ๋ฐฐํฌ ํ ํ์ธํ ์ฃผ์ ๊ฒฐ๊ณผ๊ฐ
```
#### 1. `variables.tf` (๋ณ์ ์ ์)
```hcl
variable "environment" {
type = string
description = "๋ฐฐํฌ ํ๊ฒฝ (dev, stage, prod)"
default = "dev"
validation {
condition = contains(["dev", "stage", "prod"], var.environment)
error_message = "environment ๊ฐ์ dev, stage, prod ์ค ํ๋์ฌ์ผ ํฉ๋๋ค."
}
}
variable "server_count" {
type = number
description = "์์ฑํ ์ธ์คํด์ค ์๋"
default = 2
}
variable "allowed_ips" {
type = list(string)
description = "์ ๊ทผ ํ์ฉ IP ๋์ญ ๋ชฉ๋ก"
default = ["10.0.0.0/16", "192.168.1.0/24"]
}
```
#### 2. `main.tf` (Locals ๋ฐ ๋ฆฌ์์ค ์ฐธ์กฐ)
```hcl
locals {
# ๋ณ์๋ค์ ์กฐํฉํ์ฌ ๊ณตํต ํ๊ทธ ๋ฐ ํ๋ก์ ํธ ์ ๋์ฌ ์์ฑ
service_name = "lucky-api"
app_prefix = "${local.service_name}-${var.environment}"
common_tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = local.service_name
}
}
# count ๋ฐ๋ณต๋ฌธ์ ์ฌ์ฉํ ๋ค์ค ํ์ผ ์์ฑ
resource "local_file" "server_configs" {
count = var.server_count
filename = "${path.module}/server_${count.index + 1}.conf"
content = "Server Hostname: ${local.app_prefix}-${count.index + 1}\nAllowed CIDR: ${join(", ", var.allowed_ips)}\n"
}
# ์์์ ์์กด์ฑ: server_configs ์์ฑ์ด ์๋ฃ๋ ํ ํ์ผ ์์ฝ๋ณธ ์์ฑ
resource "local_file" "manifest" {
filename = "${path.module}/deployment_manifest.json"
content = jsonencode({
prefix = local.app_prefix
total = var.server_count
tags = local.common_tags
files = local_file.server_configs[*].filename
})
# ๋ช
์์ ์์กด์ฑ์ด ํ์ํ ๊ฒฝ์ฐ depends_on ์ถ๊ฐ (๋๋ถ๋ถ์ ์์ฑ ์ฐธ์กฐ๋ก ์๋ ํด๊ฒฐ๋จ)
depends_on = [local_file.server_configs]
}
```
#### 3. `outputs.tf` (์ถ๋ ฅ๊ฐ ์ ์)
```hcl
output "deployed_files" {
description = "์์ฑ๋ ๋ชจ๋ ์๋ฒ ์ค์ ํ์ผ ๋ชฉ๋ก"
value = local_file.server_configs[*].filename
}
output "manifest_file" {
description = "์์ฑ๋ ๋งค๋ํ์คํธ ํ์ผ ๊ฒฝ๋ก"
value = local_file.manifest.filename
}
```
### ๐ป ์คํ ๋ฐ ๋ณ์ ๋์ ์ฃผ์
```bash
# 1. ๋ช
๋ น์ค์์ ๋ณ์ ์ฆ์ ์ฌ์ ์ (-var)
terraform apply -var="environment=prod" -var="server_count=3" -auto-approve
# 2. ํน์ ๋ณ์ ์ถ๋ ฅ๊ฐ๋ง ์ง์
terraform output -json deployed_files
```
---
## 5. [3๋จ๊ณ] ์ค์ ํด๋ผ์ฐ๋ ํ๋ก๋น์ ๋: AWS VPC, Security Group & EC2 ์ธ์คํด์ค
์ค๋ฌด์์ ๊ฐ์ฅ ๋๋ฆฌ ์ฐ์ด๋ ํจํด์ผ๋ก, AWS ๊ณ์ ์ **๋คํธ์ํฌ(VPC, Subnet, IGW), ๋ณด์ ๊ทธ๋ฃน(Security Group), ์ต์ Ubuntu AMI ์๋ ์กฐํ, EC2 ๊ฐ์๋จธ์ **์ ์ํด๋ฆญ์ผ๋ก ๊ตฌ์ถํฉ๋๋ค.
### ๐ ์์ ํ AWS ์ธํ๋ผ ์ฝ๋ (`aws_infra.tf`)
```hcl
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.50.0"
}
}
}
provider "aws" {
region = "ap-northeast-2" # ์์ธ ๋ฆฌ์
}
# 1. VPC ๋คํธ์ํฌ ์์ฑ
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "lucky-vpc-prod"
}
}
# 2. ํผ๋ธ๋ฆญ ์๋ธ๋ท ์์ฑ
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-2a"
map_public_ip_on_launch = true # ๊ณต์ธ IP ์๋ ํ ๋น
tags = {
Name = "lucky-public-subnet-2a"
}
}
# 3. ์ธํฐ๋ท ๊ฒ์ดํธ์จ์ด(IGW) ๋ฐ ๋ผ์ฐํ
ํ
์ด๋ธ ์ฐ๊ฒฐ
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "lucky-igw"
}
}
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = {
Name = "lucky-public-rt"
}
}
resource "aws_route_table_association" "public_assoc" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public_rt.id
}
# 4. ์น ์๋ฒ ๋ณด์ ๊ทธ๋ฃน (HTTP 80, SSH 22 ํ์ฉ)
resource "aws_security_group" "web_sg" {
name = "lucky-web-sg"
description = "Allow HTTP and SSH inbound traffic"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from anywhere"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH from admin"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # ์ค๋ฌด์์๋ ํ์ฌ ๊ณต์ธ IP๋ก ์ ํ ๊ถ์ฅ
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "lucky-web-sg"
}
}
# 5. Data Source: ์ต์ ๊ณต์ Ubuntu 22.04 LTS AMI ์๋ ๊ฒ์
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical ๊ณต์ ๊ณ์ ID
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# 6. EC2 ์ธ์คํด์ค ์์ฑ ๋ฐ ์ด๊ธฐํ ์คํฌ๋ฆฝํธ(user_data) ์ฃผ์
resource "aws_instance" "web_server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web_sg.id]
# ์๋ฒ ๊ธฐ๋ ์ Nginx ์๋ ์ค์น ๋ฐ ์ฐ์ปด ํ์ด์ง ๋ฐฐํฌ
user_data = <<-EOF
#!/bin/bash
apt-get update -y
apt-get install -y nginx
echo "<h1>Welcome to LuckyTechWorld Terraform Web Server!</h1>" > /var/www/html/index.html
systemctl enable --now nginx
EOF
tags = {
Name = "lucky-web-server"
}
}
# 7. ๋ฐฐํฌ ์๋ฃ ํ ๊ณต์ธ IP ์ถ๋ ฅ
output "web_public_ip" {
description = "์น ์๋ฒ ์ ์์ฉ ๊ณต์ธ IP ์ฃผ์"
value = aws_instance.web_server.public_ip
}
output "web_url" {
description = "๋ธ๋ผ์ฐ์ ์ ์ URL"
value = "http://${aws_instance.web_server.public_ip}"
}
```
---
## 6. [4๋จ๊ณ] ์๊ฒฉ ๋ฐฑ์๋(Remote Backend) & State Lock (S3 + DynamoDB)
๋ก์ปฌ ๋จธ์ ์ `terraform.tfstate` ํ์ผ์ ๋๊ณ ์์
ํ๋ฉด ํ์ ๊ฐ ์ํ ๋๊ธฐํ๊ฐ ๋ถ๊ฐ๋ฅํ๊ณ , ๋์์ `terraform apply`๋ฅผ ์คํํ ๊ฒฝ์ฐ ์ธํ๋ผ ์ํ๊ฐ ๊นจ์ง๋๋ค. ์ค๋ฌด์์๋ **AWS S3(์๊ฒฉ ์ ์ฅ์)**์ **DynamoDB(๋์ ์คํ ๋ฐฉ์ง ๋ถ์ฐ ๋ฝ)**๋ฅผ ๊ฒฐํฉํ์ฌ ๋ฐฑ์๋๋ฅผ ๊ตฌ์ฑํฉ๋๋ค.
```text
[S3 + DynamoDB Remote State ์ํคํ
์ฒ]
ํ์ A (terraform apply) โโโโ
โโโโบ [DynamoDB Lock ํ
์ด๋ธ] (LockID ํ๋ ์ฑ๊ณต โ ์์
์งํ)
ํ์ B (terraform apply) โโโโ โโโบ (Lock ์ถฉ๋ ๋ฐ์ ์: "Acquiring state lock: Resource busy" ์๋ฌ๋ก ์์ ์ฐจ๋จ)
โ
โผ
[S3 Bucket: tfstate]
โข AES-256 ์๋ฒ ์ํธํ
โข ๋ฒ์ ๊ด๋ฆฌ(Versioning)๋ก ์ค์ ๋ณต๊ตฌ
```
### ๐ ์๊ฒฉ ๋ฐฑ์๋ ์ค์ ์ฝ๋ (`backend.tf`)
```hcl
terraform {
backend "s3" {
bucket = "lucky-terraform-remote-states" # ์ฌ์ ์ ์์ฑ๋ S3 ๋ฒํท๋ช
key = "services/lucky-api/terraform.tfstate" # ์ ์ฅ๋ ๊ฒฝ๋ก
region = "ap-northeast-2"
encrypt = true # ์๋ฒ ์ธก ์ํธํ (KMS/AES256)
dynamodb_table = "lucky-terraform-locks" # ์ํ ๋ฝ์ฉ DynamoDB ํ
์ด๋ธ (LockID ํค ํ์)
}
}
```
### ๐ ๏ธ ํต์ฌ State ๊ด๋ฆฌ CLI ๋ช
๋ น์ด ๋ชจ์
```bash
# 1. ๋ก์ปฌ ์ํ ํ์ผ์ ์๊ฒฉ S3 ๋ฐฑ์๋๋ก ๋ง์ด๊ทธ๋ ์ด์
terraform init -migrate-state
# 2. ํ์ฌ ์ํ ํ์ผ์ ๋ฑ๋ก๋ ๋ชจ๋ ๋ฆฌ์์ค ์๋ณ์ ์กฐํ
terraform state list
# 3. ํน์ ๋ฆฌ์์ค์ ์ธ๋ถ ์ํ JSON ์์ฑ ํ์ธ
terraform state show aws_instance.web_server
# 4. ์ค์ ํด๋ผ์ฐ๋ ๋ฆฌ์์ค๋ ๋จ๊ฒจ๋๊ณ ํ
๋ผํผ ๊ด๋ฆฌ ๋์์์๋ง ์ ์ธ (State ์ญ์ )
terraform state rm aws_instance.web_server
# 5. ๊ธฐ์กด์ ์ฝ์๋ก ์๋ ์์ฑํ ํด๋ผ์ฐ๋ ๋ฆฌ์์ค๋ฅผ ํ
๋ผํผ ์ฝ๋๋ก ๊ฐ์ ธ์ค๊ธฐ (Import)
terraform import aws_instance.web_server i-0123456789abcdef0
# 6. CI/CD ์์
์ค๋จ ๋ฑ์ผ๋ก ๋ฝ์ด ํ๋ฆฌ์ง ์์ ๋ ๊ฐ์ ๋ฝ ํด์
terraform force-unlock <LOCK-ID>
```
---
## 7. [5๋จ๊ณ] ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋(Module) ํจํด & ๋ฉํฐ ํ๊ฒฝ(dev/prod) ๋ถ๋ฆฌ
๋ฐ๋ณต๋๋ ์ธํ๋ผ ๊ตฌ์ฑ(์: ์น ์๋ฒ ํด๋ฌ์คํฐ, ๋ฐ์ดํฐ๋ฒ ์ด์ค)์ **์์ ๋ชจ๋(Child Module)**๋ก ์บก์ํํ๊ณ , `environments/dev`์ `environments/prod`์์ ๊ฐ๊ธฐ ๋ค๋ฅธ ํ๋ผ๋ฏธํฐ๋ก ํธ์ถํฉ๋๋ค.
### ๐ ๊ถ์ฅ ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
```text
lucky-infrastructure/
โโโ modules/
โ โโโ ec2_web/
โ โโโ main.tf
โ โโโ variables.tf
โ โโโ outputs.tf
โโโ environments/
โโโ dev/
โ โโโ main.tf
โ โโโ variables.tf
โ โโโ terraform.tfvars
โโโ prod/
โโโ main.tf
โโโ variables.tf
โโโ terraform.tfvars
```
### ๐ 1. ๋ชจ๋ ์ ์ (`modules/ec2_web/main.tf`)
```hcl
variable "instance_name" { type = string }
variable "instance_type" { type = string, default = "t3.micro" }
variable "subnet_id" { type = string }
resource "aws_instance" "this" {
ami = "ami-0c2acfcb2ac4d02a0" # ์์ AMI
instance_type = var.instance_type
subnet_id = var.subnet_id
tags = {
Name = var.instance_name
}
}
output "instance_id" {
value = aws_instance.this.id
}
```
### ๐ 2. ๊ฐ๋ฐ ํ๊ฒฝ์์ ๋ชจ๋ ํธ์ถ (`environments/dev/main.tf`)
```hcl
module "dev_web_server" {
source = "../../modules/ec2_web"
instance_name = "lucky-web-dev"
instance_type = "t3.micro"
subnet_id = "subnet-01234567"
}
output "dev_server_id" {
value = module.dev_web_server.instance_id
}
```
---
## 8. ์ค๋ฌด ์ฃผ์์ฌํญ & ํ๋ก๋์
์ฒดํฌ๋ฆฌ์คํธ (Best Practices)
### ๐จ 1. ์ ๋ ํ์ง ๋ง์์ผ ํ ์ค์ & ํธ๋ฌ๋ธ์ํ
1. **State ํ์ผ์ Git ๋ฆฌํฌ์งํ ๋ฆฌ์ ์ปค๋ฐํ์ง ๋ง์ธ์**:
- `terraform.tfstate` ํ์ผ์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ํธ, ๋ฏผ๊ฐํ API ํ ํฐ ๋ฑ์ด **ํ๋ฌธ(Plaintext)**์ผ๋ก ๊ธฐ๋ก๋ฉ๋๋ค.
- ํ๋ก์ ํธ ๋ฃจํธ์ `.gitignore` ํ์ผ์ ๋ฐ๋์ ์๋ ํญ๋ชฉ์ ํฌํจํ์ธ์:
```gitignore
# Terraform ๋ฌด์ ๋ชฉ๋ก
.terraform/
*.tfstate
*.tfstate.*
crash.log
*.tfvars
!example.tfvars
override.tf
override.tf.json
```
2. **State Lock ์ถฉ๋ ์๋ฌ (`Error: Error acquiring the state lock`)**:
- ์ด์ ์์
์ด ๊ฐ์ ์ข
๋ฃ๋์๊ฑฐ๋ ๋ค๋ฅธ ํ์์ด ๋ฐฐํฌ ์ค์ผ ๋ ๋ฐ์ํฉ๋๋ค.
- ์ค์ ๋ก ๋ค๋ฅธ ์์
์ด ์งํ ์ค์ด ์๋์ ํ์ธํ ํ, ์๋ฌ ๋ฉ์์ง์ ํ์๋ Lock ID๋ฅผ ์ด์ฉํด ํด์ ํฉ๋๋ค:
```bash
terraform force-unlock <LOCK-ID>
```
---
### ๐ก๏ธ 2. ํ๋ก๋์
์์ ์ฅ์น: ๋ฆฌ์์ค ์๋ช
์ฃผ๊ธฐ(Lifecycle) ๊ท์น
์ค์๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ค์ ์คํ ๋ฆฌ์ง ๋ฆฌ์์ค๊ฐ ์ญ์ (`destroy`)๋๋ ์ฐธ์ฌ๋ฅผ ์ฝ๋๋ก ์์ฒ ์ฐจ๋จํฉ๋๋ค.
```hcl
resource "aws_db_instance" "production_db" {
allocated_storage = 20
engine = "mysql"
instance_class = "db.t3.medium"
db_name = "luckydb"
# ...
lifecycle {
# 1. ์ค์๋ก terraform destroy ์คํ ์ ์ญ์ ๊ฑฐ๋ถ (๊ฐ์ฅ ์ค์ํ ์์ ์ฅ์น)
prevent_destroy = true
# 2. ๋ค์ดํ์ ์์ด ๋ฆฌ์์ค๋ฅผ ๊ต์ฒดํ ๋ ์ ๋ฆฌ์์ค๋ฅผ ๋จผ์ ์์ฑ ํ ๊ธฐ์กด ๋ฆฌ์์ค ์ญ์
create_before_destroy = true
# 3. ์ธ๋ถ ์๋ํ ๋๊ตฌ๋ก ๋ณ๊ฒฝ๋ ํ๊ทธ ๋ฑ์ ๋๋ฆฌํํธ ๋ฌด์
ignore_changes = [
tags["LastUpdated"]
]
}
}
```
---
### ๐ฐ 3. ํ๋ก๋์
์ด์ & CI/CD ์ฒดํฌ๋ฆฌ์คํธ
| ์ ๊ฒ ํญ๋ชฉ | ๊ถ์ฅ ์ค์ ๋ฐ ์ค๋ฌด ๊ฐ์ด๋ |
| :--- | :--- |
| **์ฝ๋ ํฌ๋งท ๋ฐ ๋ฌธ๋ฒ ๊ฒ์ฆ** | ์ปค๋ฐ ์ ํญ์ `terraform fmt -recursive` ๋ฐ `terraform validate`๋ฅผ ์คํํ์ฌ ํ ์ปจ๋ฒค์
์ ํต์ผํฉ๋๋ค. |
| **๋๋ฆฌํํธ ๊ฐ์ง (Drift Detection)** | ์ฝ์์์ ๋๊ตฐ๊ฐ ์๋์ผ๋ก ๋ฆฌ์์ค๋ฅผ ๋ณ๊ฒฝํ๋์ง ์ ๊ธฐ์ ์ผ๋ก ๊ฐ์งํฉ๋๋ค: `terraform plan -detailed-exitcode`. |
| **CI/CD ํ์ดํ๋ผ์ธ ๋ถ๋ฆฌ** | Pull Request ๋จ๊ณ์์๋ `terraform plan` ๊ฒฐ๊ณผ๋ฅผ PR ์ฝ๋ฉํธ๋ก ์๋ ์ถ๋ ฅํ๊ณ , Main ๋ธ๋์น ๋จธ์ง ์์๋ง `terraform apply`๋ฅผ ์๋ ์คํํฉ๋๋ค. |
| **๋น์ฉ ์์ธก ๋๊ตฌ (Infracost)** | PR ๋จ๊ณ์์ ์ธํ๋ผ ๋ณ๊ฒฝ์ ๋ฐ๋ฅธ ์์ AWS ์ ์ฒญ๊ตฌ ๋น์ฉ ์ฆ๊ฐ($)์ ์๋์ผ๋ก ๊ณ์ฐํ์ฌ ๊ณผ๋ํ ์ธ์คํด์ค ๊ณผ๊ธ์ ์ฌ์ ์ ์ฐจ๋จํฉ๋๋ค. |
| **๋ณด์ ์ ์ ๋ถ์ (Tfsec / Trivy)** | `trivy config .` ๋๋ `tfsec .` ๋๊ตฌ๋ฅผ ์คํํ์ฌ ์ด๋ ค์๋ 0.0.0.0/0 ๋ณด์ ๊ทธ๋ฃน์ด๋ ์ํธํ๋์ง ์์ S3 ๋ฒํท์ ๋ฐฐํฌ ์ ์ ์๋ ๊ฐ์งํฉ๋๋ค. |
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์