테라폼 - S3 구현하기
Bucket, Public Access Block, Versioning, Encryption, Lifecycle까지 안전하게 구성하기
이전 글에서는 Terraform으로 EC2를 구현하는 방법을 정리했다.
이번 글에서는 AWS에서 가장 자주 사용하는 저장소 서비스인 S3를 Terraform으로 구현해보려 한다.
S3는 처음 보면 단순한 파일 저장소처럼 보인다. 하지만 실제로 운영 환경에서 사용하려면 단순히 버킷 하나를 만드는 것으로 끝나지 않는다.
S3 Bucket
Public Access Block
Versioning
Server-Side Encryption
Lifecycle Rule
Bucket Policy
force_destroy
prevent_destroy
특히 S3는 실수로 공개되거나, 삭제되면 안 되는 데이터를 삭제하거나, 오래된 객체가 계속 쌓여 비용이 증가하는 문제가 생기기 쉽다.
S3는 만들기보다 안전하게 공개하지 않고, 안전하게 보존하고, 비용이 쌓이지 않게 관리하는 것이 더 중요하다.
목차
- 1. S3란 무엇인가
- 2. S3를 만들 때 함께 고려할 것들
- 3. 기본 S3 Bucket 생성
- 4. Public Access Block 설정
- 5. Versioning 설정
- 6. Server-Side Encryption 설정
- 7. Lifecycle Rule 설정
- 8. Bucket Policy 설정
- 9. force_destroy와 prevent_destroy
- 10. 실전 예제: 일반 애플리케이션 파일 저장용 S3
- 11. 특수 케이스 1: Terraform Backend용 S3
- 12. 특수 케이스 2: CloudFront Origin용 S3
- 13. S3 의존성 흐름
- 14. 자주 하는 실수
- 15. 마무리
1. S3란 무엇인가
S3는 Simple Storage Service의 약자다.
쉽게 말하면 AWS에서 제공하는 객체 스토리지다.
파일을 디렉토리 구조처럼 저장한다고 생각하기 쉽지만, 정확히는 객체를 버킷 안에 저장하는 방식이다.
S3 Bucket
└── Object
├── key
├── data
└── metadata
S3는 다음과 같은 용도로 많이 사용된다.
이미지 저장
첨부파일 저장
로그 저장
백업 파일 저장
정적 웹사이트 파일 저장
Terraform state 저장
CloudFront Origin
S3는 매우 범용적인 저장소이기 때문에 사용 범위가 넓다.
그만큼 보안 설정도 중요하다.
2. S3를 만들 때 함께 고려할 것들
Terraform으로 S3를 구성할 때는 단순히 aws_s3_bucket만 만들지 않는 것이 좋다.
보통 다음 리소스들을 함께 고려한다.
| 리소스 | 역할 |
| aws_s3_bucket | S3 Bucket 생성 |
| aws_s3_bucket_public_access_block | 버킷 공개 접근 차단 |
| aws_s3_bucket_versioning | 객체 버전 관리 |
| aws_s3_bucket_server_side_encryption_configuration | 서버 측 암호화 설정 |
| aws_s3_bucket_lifecycle_configuration | 오래된 객체 또는 버전 정리 |
| aws_s3_bucket_policy | 버킷 접근 정책 |
| aws_s3_bucket_ownership_controls | 객체 소유권 제어 |
초보자라면 다음 기준으로 시작하면 좋다.
기본은 private bucket
Public Access Block 활성화
Versioning 활성화
Encryption 명시
Lifecycle로 오래된 버전 정리
Bucket Policy는 필요한 경우에만 작성
3. 기본 S3 Bucket 생성
가장 기본적인 S3 Bucket은 다음처럼 만들 수 있다.
resource "aws_s3_bucket" "app" {
bucket = "${var.project_name}-app-bucket"
tags = merge(local.common_tags, {
Name = "${var.project_name}-app-bucket"
})
}
여기서 bucket은 버킷 이름이다.
S3 버킷 이름은 전 세계에서 유일해야 한다.
bucket = "${var.project_name}-app-bucket"
하지만 위 코드만으로는 운영 환경에 사용하기 부족하다.
S3는 보안과 데이터 보호 설정을 함께 구성해야 한다.
4. Public Access Block 설정
S3에서 가장 먼저 신경 써야 하는 것은 공개 접근 차단이다.
S3 버킷이 실수로 공개되면 민감한 파일이 외부에 노출될 수 있다.
따라서 일반적인 애플리케이션 파일 저장소는 Public Access Block을 켜는 것이 좋다.
resource "aws_s3_bucket_public_access_block" "app" {
bucket = aws_s3_bucket.app.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
각 설정의 의미는 다음과 같다.
| 설정 | 의미 |
| block_public_acls | 공개 ACL 설정 차단 |
| ignore_public_acls | 기존 공개 ACL 무시 |
| block_public_policy | 공개 Bucket Policy 차단 |
| restrict_public_buckets | 공개 정책이 있어도 접근 제한 |
일반적인 private bucket에서는 네 값을 모두 true로 두는 것이 안전하다.
S3는 기본적으로 공개하지 않는 방향으로 설계하는 것이 좋다.
5. Versioning 설정
Versioning은 S3 객체의 여러 버전을 보관하는 기능이다.
예를 들어 같은 key에 파일을 다시 업로드하면 기존 객체를 덮어쓰는 것이 아니라, 이전 버전을 함께 보관할 수 있다.
file.txt v1
file.txt v2
file.txt v3
Terraform 코드는 다음과 같다.
resource "aws_s3_bucket_versioning" "app" {
bucket = aws_s3_bucket.app.id
versioning_configuration {
status = "Enabled"
}
}
Versioning을 켜면 실수로 파일을 덮어썼을 때 복구할 가능성이 생긴다.
하지만 버전이 계속 쌓이면 비용도 증가한다.
따라서 Versioning을 켠다면 Lifecycle Rule도 함께 고려하는 것이 좋다.
Versioning
→ 데이터 보호
Lifecycle
→ 오래된 버전 비용 관리
6. Server-Side Encryption 설정
S3에 저장되는 객체는 암호화해서 보관하는 것이 좋다.
AWS에서 기본 암호화를 제공하더라도, Terraform 코드에서 암호화 설정을 명시해두면 의도를 분명하게 표현할 수 있다.
가장 기본적인 SSE-S3 암호화 설정은 다음과 같다.
resource "aws_s3_bucket_server_side_encryption_configuration" "app" {
bucket = aws_s3_bucket.app.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
이 설정은 Amazon S3 관리형 키를 사용한 서버 측 암호화다.
SSE-S3
→ sse_algorithm = "AES256"
KMS 키를 사용하고 싶다면 다음처럼 설정할 수 있다.
resource "aws_kms_key" "s3" {
description = "KMS key for S3 bucket encryption"
deletion_window_in_days = 7
tags = merge(local.common_tags, {
Name = "${var.project_name}-s3-kms-key"
})
}
resource "aws_s3_bucket_server_side_encryption_configuration" "app" {
bucket = aws_s3_bucket.app.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3.arn
}
}
}
다만 KMS를 사용하면 권한 관리가 추가로 필요하다.
초보 단계에서는 먼저 SSE-S3로 시작하고, KMS가 필요한 경우 별도로 확장하는 것이 좋다.
7. Lifecycle Rule 설정
S3는 객체가 계속 쌓이면 비용이 증가한다.
특히 Versioning을 켠 경우 오래된 버전이 계속 남을 수 있다.
Lifecycle Rule을 사용하면 오래된 객체나 오래된 버전을 자동으로 정리할 수 있다.
resource "aws_s3_bucket_lifecycle_configuration" "app" {
bucket = aws_s3_bucket.app.id
rule {
id = "delete-old-noncurrent-versions"
status = "Enabled"
filter {
prefix = ""
}
noncurrent_version_expiration {
noncurrent_days = 30
}
}
}
위 설정은 현재 버전이 아닌 오래된 버전을 30일 후 삭제한다.
noncurrent version
→ 현재 버전이 아닌 이전 버전
로그 저장용 버킷이라면 일정 기간이 지난 객체를 삭제할 수도 있다.
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
rule {
id = "delete-old-logs"
status = "Enabled"
filter {
prefix = ""
}
expiration {
days = 90
}
}
}
이 설정은 90일이 지난 객체를 삭제한다.
Versioning은 데이터를 보호하고, Lifecycle은 비용을 관리한다.
8. Bucket Policy 설정
Bucket Policy는 S3 Bucket에 대한 접근 정책이다.
예를 들어 특정 IAM Role만 버킷의 객체를 읽을 수 있도록 허용할 수 있다.
data "aws_iam_policy_document" "app_bucket_read" {
statement {
effect = "Allow"
principals {
type = "AWS"
identifiers = [aws_iam_role.app.arn]
}
actions = [
"s3:GetObject"
]
resources = [
"${aws_s3_bucket.app.arn}/*"
]
}
}
resource "aws_s3_bucket_policy" "app" {
bucket = aws_s3_bucket.app.id
policy = data.aws_iam_policy_document.app_bucket_read.json
}
다만 일반적인 private bucket에서는 반드시 Bucket Policy가 필요한 것은 아니다.
EC2, ECS, Lambda 같은 리소스가 S3에 접근해야 한다면 보통 해당 리소스의 IAM Role에 S3 접근 권한을 부여하는 방식으로도 충분하다.
EC2 Role
→ s3:GetObject 허용
S3 Bucket
→ Public Access Block 유지
Bucket Policy는 다음 상황에서 자주 사용한다.
CloudFront만 S3에 접근 허용
특정 AWS 계정만 접근 허용
특정 VPC Endpoint를 통한 접근만 허용
특정 조건의 요청만 허용
강제 암호화 업로드 정책
즉, Bucket Policy는 버킷 자체의 접근 경계를 만들 때 사용한다.
9. force_destroy와 prevent_destroy
S3는 삭제할 때 자주 문제가 생긴다.
버킷 안에 객체가 남아 있으면 기본적으로 버킷 삭제가 실패할 수 있다.
BucketNotEmpty
Terraform에서는 force_destroy를 사용할 수 있다.
resource "aws_s3_bucket" "app" {
bucket = "${var.project_name}-app-bucket"
force_destroy = true
tags = merge(local.common_tags, {
Name = "${var.project_name}-app-bucket"
})
}
force_destroy = true를 사용하면 버킷 삭제 시 내부 객체까지 함께 삭제될 수 있다.
학습용 버킷에는 편리할 수 있다.
하지만 운영 데이터가 들어 있는 버킷에는 매우 위험할 수 있다.
학습용:
force_destroy = true 가능
운영 데이터:
force_destroy = false 권장
중요한 버킷은 prevent_destroy로 보호할 수도 있다.
resource "aws_s3_bucket" "app" {
bucket = "${var.project_name}-app-bucket"
lifecycle {
prevent_destroy = true
}
tags = merge(local.common_tags, {
Name = "${var.project_name}-app-bucket"
})
}
이 설정은 Terraform이 해당 버킷을 삭제하려고 할 때 오류를 발생시킨다.
운영 데이터 버킷은 삭제 편의성보다 삭제 방지가 더 중요하다.
10. 실전 예제: 일반 애플리케이션 파일 저장용 S3
이제 일반 애플리케이션에서 첨부파일이나 이미지를 저장하는 private S3 Bucket 예제를 정리해보자.
10.1 variables.tf
variable "project_name" {
description = "Project name"
type = string
}
variable "bucket_name" {
description = "S3 bucket name"
type = string
}
10.2 terraform.tfvars
project_name = "demo"
bucket_name = "demo-app-files-123456"
S3 버킷 이름은 전 세계에서 유일해야 하므로 프로젝트 이름만으로는 중복될 수 있다.
실무에서는 계정 ID, 환경명, 랜덤 suffix 등을 함께 사용하는 경우가 많다.
10.3 locals.tf
locals {
common_tags = {
Project = var.project_name
ManagedBy = "terraform"
}
}
10.4 main.tf
resource "aws_s3_bucket" "app" {
bucket = var.bucket_name
force_destroy = false
tags = merge(local.common_tags, {
Name = var.bucket_name
})
}
resource "aws_s3_bucket_public_access_block" "app" {
bucket = aws_s3_bucket.app.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_versioning" "app" {
bucket = aws_s3_bucket.app.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "app" {
bucket = aws_s3_bucket.app.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_lifecycle_configuration" "app" {
bucket = aws_s3_bucket.app.id
rule {
id = "delete-old-noncurrent-versions"
status = "Enabled"
filter {
prefix = ""
}
noncurrent_version_expiration {
noncurrent_days = 30
}
}
}
이 구성은 다음 성격을 가진다.
Private Bucket
Public Access Block 활성화
Versioning 활성화
SSE-S3 암호화 명시
오래된 이전 버전 30일 후 삭제
force_destroy 비활성화
10.5 outputs.tf
output "s3_bucket_id" {
description = "S3 bucket ID"
value = aws_s3_bucket.app.id
}
output "s3_bucket_arn" {
description = "S3 bucket ARN"
value = aws_s3_bucket.app.arn
}
output "s3_bucket_name" {
description = "S3 bucket name"
value = aws_s3_bucket.app.bucket
}
이 output 값은 이후 IAM Policy나 애플리케이션 설정에서 사용할 수 있다.
IAM Policy
→ S3 Bucket ARN 참조
Application
→ S3 Bucket Name 사용
11. 특수 케이스 1: Terraform Backend용 S3
S3는 Terraform state를 저장하는 backend로도 많이 사용된다.
Terraform
→ S3 Bucket에 state 저장
Backend용 S3는 일반 애플리케이션 파일 저장소보다 더 조심해서 다뤄야 한다.
보통 다음 설정을 고려한다.
Public Access Block
Versioning
Encryption
prevent_destroy
접근 권한 제한
Backend용 S3 예시는 다음과 같다.
resource "aws_s3_bucket" "tfstate" {
bucket = "${var.project_name}-tfstate"
lifecycle {
prevent_destroy = true
}
tags = merge(local.common_tags, {
Name = "${var.project_name}-tfstate"
})
}
resource "aws_s3_bucket_public_access_block" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
Terraform backend 설정은 별도 파일에 다음처럼 작성할 수 있다.
terraform {
backend "s3" {
bucket = "demo-tfstate"
key = "dev/terraform.tfstate"
region = "ap-northeast-2"
}
}
주의할 점은 backend용 S3 Bucket은 Terraform이 state를 저장하기 전에 먼저 존재해야 한다는 것이다.
따라서 backend bucket은 별도 bootstrap 과정으로 만들거나, 처음에는 local state로 만든 뒤 backend로 전환하는 방식을 사용할 수 있다.
12. 특수 케이스 2: CloudFront Origin용 S3
정적 파일을 S3에 두고 CloudFront로 배포하는 구조도 많이 사용된다.
User
→ CloudFront
→ S3 Origin
이 경우 S3를 직접 public으로 여는 방식은 권장하지 않는다.
대신 S3는 private으로 두고, CloudFront만 S3에 접근할 수 있도록 구성하는 것이 좋다.
권장 구조:
S3 Public Access Block 활성화
CloudFront Origin Access Control 사용
Bucket Policy로 CloudFront만 허용
이 구성은 CloudFront 글에서 더 자세히 다루는 것이 좋다.
여기서는 핵심만 기억하면 된다.
S3 정적 파일을 외부에 제공할 때도 S3를 직접 공개하기보다 CloudFront를 앞에 두는 구성이 더 안전하다.
13. S3 의존성 흐름
S3 Bucket과 관련 설정의 의존성 흐름은 다음과 같다.
S3 Bucket
├── Public Access Block
├── Versioning
├── Encryption
├── Lifecycle Configuration
└── Bucket Policy

S3 Bucket 자체는 독립적으로 만들 수 있지만, 보안과 운영을 위해 여러 설정 리소스를 함께 연결하는 것이 일반적이다.
앞선 분류 기준으로 보면 S3 Bucket은 Independent에 가깝고, Public Access Block, Versioning, Encryption, Lifecycle, Policy는 S3 Bucket을 참조하는 Dependent 리소스라고 볼 수 있다.
14. 자주 하는 실수
14.1 버킷을 public으로 열어둠
가장 위험한 실수 중 하나다.
특별한 이유가 없다면 S3 Bucket은 private으로 시작하는 것이 좋다.
Public Access Block
→ 기본 활성화 권장
14.2 Bucket Policy로 public 허용 후 Public Access Block 때문에 안 된다고 헷갈림
S3 Public Access Block이 켜져 있으면 public Bucket Policy를 작성해도 실제 공개 접근이 차단될 수 있다.
정적 웹사이트 호스팅처럼 public 접근이 필요한 경우에는 Public Access Block 설정과 Bucket Policy를 함께 이해해야 한다.
다만 일반적인 운영에서는 public 접근을 열기보다 CloudFront를 앞에 두는 것이 더 안전하다.
14.3 Versioning만 켜고 Lifecycle을 설정하지 않음
Versioning은 데이터 보호에 도움이 된다.
하지만 오래된 버전이 계속 쌓이면 비용이 증가한다.
Versioning 활성화
→ 이전 버전 계속 보관
→ 비용 증가 가능
→ Lifecycle 필요
14.4 force_destroy를 운영 버킷에 사용함
force_destroy = true는 학습용이나 임시 버킷에는 편리하다.
하지만 운영 데이터가 들어 있는 버킷에 사용하면 위험하다.
terraform destroy
→ 버킷 내부 객체까지 삭제 가능
중요 데이터 버킷에는 force_destroy = false와 prevent_destroy를 고려하는 것이 좋다.
14.5 Secret이나 Access Key를 S3에 평문 저장
S3에 민감정보를 저장해야 한다면 암호화와 접근 권한을 매우 엄격하게 관리해야 한다.
일반적으로 애플리케이션 Secret은 S3보다 Secrets Manager나 SSM Parameter Store를 사용하는 것이 더 적합하다.
14.6 Bucket Name을 대충 정함
S3 Bucket 이름은 전 세계에서 유일해야 한다.
따라서 단순히 app-bucket 같은 이름은 이미 사용 중일 가능성이 높다.
권장 예:
project-env-purpose-account-id
예를 들면 다음과 같다.
demo-dev-app-files-123456789012
14.7 Terraform Backend Bucket을 일반 리소스와 함께 삭제함
Terraform state를 저장하는 S3 Bucket은 매우 중요하다.
Backend용 S3를 일반 app 리소스와 같은 생명주기로 관리하면 위험하다.
app destroy
→ tfstate bucket 삭제 시도
→ state 관리 문제 발생
Backend용 S3는 별도 bootstrap 계층으로 분리하고, 삭제 방지 설정을 두는 것이 좋다.
15. 마무리
이번 글에서는 Terraform으로 S3를 구현하는 방법을 정리했다.
S3는 단순한 파일 저장소처럼 보이지만, 실제 운영에서는 다음 설정을 함께 고려해야 한다.
Public Access Block
Versioning
Encryption
Lifecycle
Bucket Policy
force_destroy
prevent_destroy
초보자라면 먼저 private bucket을 안전하게 만드는 구조부터 익히는 것이 좋다.
이후 필요에 따라 Terraform backend용 S3, CloudFront Origin용 S3, 로그 저장용 S3 등으로 확장하면 된다.
한 줄 정리
S3는 버킷을 만드는 것보다 공개 차단, 보존, 암호화, 비용 관리를 함께 설계하는 것이 중요하다.
다음 글에서는 RDS MySQL을 Terraform으로 구현해본다. RDS는 DB Subnet Group, Security Group, 백업, 삭제 보호, 비밀번호 관리까지 함께 고려해야 하는 리소스다.
'테라폼' 카테고리의 다른 글
| 4-8. 테라폼 - Secrets Manager와 SSM Parameter Store 구현하기 (0) | 2026.05.13 |
|---|---|
| 4-7. 테라폼 - RDS MySQL 구현하기 (0) | 2026.05.13 |
| 4-5. 테라폼 - EC2 구현하기 (0) | 2026.05.12 |
| 4-4. 테라폼 - IAM Role과 Policy 구현하기 (0) | 2026.05.12 |
| 4-3. 테라폼 - Security Group과 네트워크 보안 설정 (0) | 2026.05.12 |