테라폼

4-6. 테라폼 - S3 구현하기

pininini 2026. 5. 13. 15:02

테라폼 - 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 = falseprevent_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, 백업, 삭제 보호, 비밀번호 관리까지 함께 고려해야 하는 리소스다.