현재 상황 상황
이미 EC2, RDS, VPC 등 대부분의 AWS 리소스를 사용하고 있었고, 현재 Terraform으로 하나씩 전환을 진행했습니다. 그중 한 번은 기록을 남기는 게 좋다는 생각이 들었고, 마지막으로 남아있는 ECR 저장소를 Infrastructure as Code로 전환하며 글을 남기기로 했습니다.
Terraformer를 사용하지 않은 이유
고민했던 포인트
- 창업 단계의 스타트업이라 AWS Resource 개수가 많지 않았고, 모두 파악이 가능했습니다.
- 예를 들어 ECR 저장소는 2개밖에 되지 않았습니다.
- AWS CLI 학습 기회라고 생각했습니다.
1. Terraform 준비
도구 설치 확인
# AWS CLI 버전 확인
~ aws --version
aws-cli/2.25.13 Python/3.12.9 Darwin/23.5.0 exe/x86_64
# Terraform 버전 확인
~ terraform --version
Terraform v1.13.1 # 최신 버전이 아닙니다. (저는 이전 회사에서 이미 사용하며 설치했기 때문에 이전버전입니다.)
on darwin_arm64
AWS 자격 검토
# 1. AWS 자격 증명 상태 확인 (계정 ID는 마스킹 처리)
aws sts get-caller-identity
{
"UserId": "AIXXXXXXXXXXXXXXXXXX",
"Account": "123456789012", # 마스킹 처리된 예시
"Arn": "arn:aws:iam::123456789012:user/your-username"
}
# 2. 최소 권한 설정
aws iam get-user --user-name your-username
필수 권한 목록
ecr:DescribeRepositories- ECR 저장소 조회ecr:GetRepositoryPolicy- 정책 확인ecr:ListTagsForResource- 태그 조회ecr:TagResource- 태그 수정 (필요시)
ECR에 해당 되는 권한이므로, 추가 Resource에 따른 권한도 검토해야 합니다.
AWS Profile 설정 및 리전 설정
# 환경 변수로 리전 설정 (하드코딩 방지)
export AWS_DEFAULT_REGION=ap-northeast-2
# AWS CLI 프로파일 사용 (권장)
aws configure --profile production
aws configure list --profile production
2. ECR 인프라 확인
저장소 목록 조회
# ECR 저장소 전체 목록 확인
aws ecr describe-repositories --region $AWS_DEFAULT_REGION
# ECR 저장소 전체 목록 확인 (DEFAULT REGION을 설정하지 않았을 경우)
aws ecr describe-repositories --region ap-northeast-2
실제 응답 예시 (마스킹 적용)
{
"repositories": [
{
"repositoryArn": "arn:aws:ecr:ap-northeast-2:XXXXXXXXXXXX:repository/lambda-service",
"registryId": "XXXXXXXXXXXX",
"repositoryName": "lambda-service",
"repositoryUri": "XXXXXXXXXXXX.dkr.ecr.ap-northeast-2.amazonaws.com/lambda-service",
"createdAt": "2024-09-01T10:30:00+09:00",
"imageTagMutability": "MUTABLE",
"imageScanningConfiguration": {
"scanOnPush": false
},
"encryptionConfiguration": {
"encryptionType": "AES256"
}
},
{
"repositoryArn": "arn:aws:ecr:ap-northeast-2:XXXXXXXXXXXX:repository/domain/solution",
"registryId": "XXXXXXXXXXXX",
"repositoryName": "domain/solution",
"repositoryUri": "XXXXXXXXXXXX.dkr.ecr.ap-northeast-2.amazonaws.com/domain/solution",
"createdAt": "2024-09-15T14:20:00+09:00",
"imageTagMutability": "MUTABLE",
"imageScanningConfiguration": {
"scanOnPush": true
},
"encryptionConfiguration": {
"encryptionType": "AES256"
}
}
]
}
정책 확인
# 각 저장소별 접근 정책 확인
aws ecr get-repository-policy --repository-name lambda-service
정책이 있는 경우에는 아래처럼 응답을 받을 수 있습니다.
{
"registryId": "XXXXXXXXXXXX",
"repositoryName": "lambda-service",
"policyText": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"AllowPull\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::XXXXXXXXXXXX:role/ECSTaskRole\"},\"Action\":[\"ecr:GetDownloadUrlForLayer\",\"ecr:BatchGetImage\",\"ecr:BatchCheckLayerAvailability\"]}]}"
}
정책이 없는 경우에는 아래처럼 에러를 반환받습니다.
An error occurred (RepositoryPolicyNotFoundException) when calling the GetRepositoryPolicy operation: Repository policy does not exist
보안 정책에서 확인해야 할 부분
- 접근 권한이 적절한 주체에게만 부여되었는지 확인
- 과도한 권한 부여 여부 검토
- 정책의 조건(Condition) 설정 확인
3. Terraform 코드 작성
프로젝트 구조 설정
# 보안을 고려한 디렉토리 구조
mkdir -p terraform/environments/prod/ecr
cd terraform/environments/prod/ecr
그리고, git을 사용하신다면, 설정해야 하는 gitignore 내용입니다.
# .gitignore 설정 (민감정보 보호)
# Terraform state files
*.tfstate
*.tfstate.*
# Terraform variable files with sensitive data
terraform.tfvars
*.auto.tfvars
# Terraform plans
*.tfplan
# Local Terraform directories
.terraform/
.terraform.lock.hcl
변수 파일 작성
variables.tf
variable "lifecycle_image_count" {
description = "Number of images to keep in ECR repositories"
type = number
default = 5
validation {
condition = var.lifecycle_image_count > 0 && var.lifecycle_image_count <= 100
error_message = "Lifecycle image count must be between 1 and 100."
}
}
variable "common_tags" {
description = "Common tags to apply to all ECR resources"
type = map(string)
default = {
Environment = "prod"
ManagedBy = "terraform"
}
}
메인 Terraform 코드 작성
main.tf
terraform {
required_version = ">= 1.13.0"
backend "s3" {
bucket = "infrastructure"
key = "terraform/v1/prod/resources/ecr"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-lock"
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.80"
}
}
}
provider "aws" {
region = "ap-northeast-2"
}
# ECR Repository: Serverless Lambda
resource "aws_ecr_repository" "lambda-service" {
name = "lambda-service"
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = false
}
encryption_configuration {
encryption_type = "AES256"
}
tags = merge(var.common_tags, {
Name = "lambda-service"
Purpose = "serverless-lambda"
})
}
# ECR Repository: solution app
resource "aws_ecr_repository" "domain_solution" {
name = "domain/solution"
image_tag_mutability = "MUTABLE" # 이미 활성화된 설정
image_scanning_configuration {
scan_on_push = false # 이미 활성화된 설정
}
encryption_configuration {
encryption_type = "AES256"
}
tags = merge(var.common_tags, {
Name = "domain_solution"
Purpose = "solution-service"
})
}
4. 안전한 Import 실행
초기화 및 검증
# Terraform 초기화
terraform init
# 문법 검증
terraform validate
# 포맷팅 정리
terraform fmt
Import 전 상태 확인
# 현재 상태 확인 (비어있어야 정상)
terraform state list
# Plan 실행 (모든 리소스가 "create"로 표시되어야 함)
terraform plan -var-file="terraform.tfvars"
Import를 진행하기 이전이므로, Plan에서 나오는 값들이 모두 create로 뜨는 것이 정상입니다.
이후 Import를 진행하면 변경 사항이 없거나, update (tags 추가 등의 리소스에 영향을 주지 않는 부분)만 등장해야 합니다.
destroy가 보인다면 다시 검토가 필요합니다.
단계별 Import 실행
첫 번째 저장소 Import를 진행합니다.
terraform import -var-file="terraform.tfvars" \
aws_ecr_repository.domain_solution \
"domain/solution"
성공 시 아래처럼 응답이 오게 됩니다.
aws_ecr_repository.domain_solution: Importing from ID "domain/solution"...
aws_ecr_repository.domain_solution: Import prepared!
aws_ecr_repository.domain_solution: Refreshing state... [id=domain/solution]
Import successful!
두 번째 저장소도 동일하게 Import를 진행합니다.
terraform import -var-file="terraform.tfvars" \
aws_ecr_repository.lambda-service \
lambda-service
정책이 존재한다면, Import
terraform import -var-file="terraform.tfvars" \
aws_ecr_repository_policy.lambda-service_policy \
lambda-service
Import 후 검증
# 모든 리소스가 등록되었는지 확인
terraform state list
# 예상 출력:
# aws_ecr_repository.domain_solution
# aws_ecr_repository.lambda-service
# aws_ecr_repository_policy.lambda-service_policy
"Resource already managed" 오류가 발생할 경우
이슈 재현
실제 작업 중 다음과 같은 오류가 발생할 수 있습니다:
$ terraform import aws_ecr_repository.domain_solution domain_solution
Error: Resource already managed by Terraform
│
│ Terraform is already managing a remote object for aws_ecr_repository.domain_solution.
│ To import to this address you must first remove the existing object from the state.
해당 이슈는 Terraform의 상태 파일(. tfstate)에 해당 리소스 주소(aws_ecr_repository.domain_solution)가 이미 등록되어 있을 때 발생합니다. 이전에 import를 시도했으나 실패했거나, 다른 경로로 상태가 이미 기록된 경우에 나타날 수 있습니다.
Plan을 진행하고, 의도한 바와 같은 결괏값을 주고 있다면 Apply를 진행하면 되지만, 의도치 않게 상태가 꼬인 경우, terraform state rm aws_ecr_repository.domain_solution 명령어로 상태 파일에서 해당 리소스를 먼저 제거한 후 다시 import를 진행하면 됩니다.
5. 적용 및 최종 검증
변경사항 적용
# Plan으로 변경사항 미리 확인
terraform plan -var-file="terraform.tfvars"
# 출력 예시:
# Plan: 0 to add, 2 to change, 0 to destroy.
# 태그 추가로 인한 in-place 업데이트 (안전함)
# 변경사항 적용
terraform apply -var-file="terraform.tfvars"
성공 확인
# 최종 상태 확인
terraform plan -var-file="terraform.tfvars"
# 출력: No changes. Your infrastructure matches the configuration.
# AWS CLI로 실제 상태 확인
aws ecr describe-repositories --repository-names lambda-service \
--query 'repositories[0].tags' --output table
동기화 이후 추가 설정
생명주기 정책 추가
ECR에는 Image가 많이 쌓일수록 비용이 발생할 수 있는 구조이므로, 일정 개수 이상 발생하면 자동으로 정리를 해주는 생명주기 정책을 설정할 수 있습니다. 이 또한 Code를 통해 관리할 수 있으므로, 정책을 추가해 보도록 하겠습니다.
resource "aws_ecr_lifecycle_policy" "lambda-service_policy" {
repository = aws_ecr_repository.lambda-service.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Keep last ${var.lifecycle_image_count} images (any tag)"
selection = { tagStatus = "any", countType = "imageCountMoreThan", countNumber = var.lifecycle_image_count }
action = { type = "expire" }
}
]
})
}
resource "aws_ecr_lifecycle_policy" "domain_solution_policy" {
repository = aws_ecr_repository.domain_solution.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Keep last ${var.lifecycle_image_count} images (any tag)"
selection = { tagStatus = "any", countType = "imageCountMoreThan", countNumber = var.lifecycle_image_count }
action = { type = "expire" }
}
]
})
}
후기
ECR 전환을 마지막으로, 서비스의 거의 모든 인프라가 Terraform 하에 관리되도록 전환했습니다.
사실, 1인 개발과 빠른 속도를 중시하는 스타트업 환경에서 IaC로 전환하는 태스크를 선택하는 것이 처음에는 고민이었습니다. 전환하는 시간조차도 사치일 수 있겠다는 생각이 들었기 때문입니다. 하지만, 모든 것을 혼자 검토해야 하는 1인 개발, 스타트업이라는 환경에서 스스로를 검토하고 제어할 시스템적 안전장치가 필요하다고 생각했습니다.
해당 작업을 진행했기 때문에, 모든 인프라 변경은 Pull Request(PR)로부터 시작됩니다. PR에 첨부된 terraform plan은 다시 검토할 수 있는 환경을 마련하고 Merge는 통제된 방식의 apply를 실행합니다. 무의식적으로 입력하는 명령어가 아니라, 무심코 다음 단계의 명령어를 입력하는 것이 아니라 단계와 절차를 밟아야 하기 때문에, 실수가 발생하기 어려운 환경이 되었습니다.
이제 PR과 Git 로그가 인프라 설정의 이유를 설명하는 '설계 문서'가 되고, 모든 변경 이력을 추적할 수 있습니다. 추후에 팀원이 늘어나더라도, 특정 리소스의 용도를 파악하기 위해 그 히스토리를 아는 누군가를 찾아다닐 필요가 없어질 거라 생각합니다. (팀원이 생기는 그날까지 파이팅..)
이제 추가적으로 모듈화와 변수 활용을 더 잘하는 방법으로 개선하면 될 것 같습니다.
저는 Action에 Plan과 Merge에 따른 추가 전략을 덧붙였습니다. (PR 단계에서 Dry Run시도, 변경 사항에 대한 PR Comment, Apply 이후 문서 작성 자동화 등)
PR 단계에서의 검토


Merge 결과


'Server > Infra' 카테고리의 다른 글
| EC2환경에서의 무중단 배포, ubuntu nginx setup 기록 (0) | 2025.09.24 |
|---|---|
| N100 미니PC를 이용한 나만의 서버 구축기 - 3 / K3s 설치 (0) | 2025.05.29 |
| N100 미니PC를 이용한 나만의 서버 구축기 - 2 / VPN 설정 (0) | 2025.05.27 |
| N100 미니PC를 이용한 나만의 서버 구축기 - 1 / Proxmox 설정 (1) | 2025.05.27 |
| SQS 메시지를 한 번만 처리하기 위한 고민 (FIFO, DB 멱등성, 그리고 현실적 고려사항) (0) | 2025.05.13 |