<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>인프라 개발기</title>
    <link>https://pininininfra.tistory.com/</link>
    <description>pininininfra 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 12 Jun 2026 11:53:19 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>pininini</managingEditor>
    <item>
      <title>4-12. 테라폼 - Lambda와 EventBridge 구현하기</title>
      <link>https://pininininfra.tistory.com/24</link>
      <description>&lt;h1 style=&quot;text-align:center;&quot;&gt;테라폼 - Lambda와 EventBridge 구현하기&lt;/h1&gt;
&lt;p style=&quot;text-align:center;&quot;&gt;&lt;em&gt;서버 없이 코드를 실행하고, 이벤트나 일정에 따라 자동으로 호출하기&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;
이전 글에서는 Terraform으로 ECS Fargate를 구현하는 방법을 정리했다.
&lt;/p&gt;

&lt;p&gt;
이번 글에서는 AWS의 서버리스 실행 환경인 &lt;strong&gt;Lambda&lt;/strong&gt;와 이벤트 기반 실행을 위한 &lt;strong&gt;EventBridge&lt;/strong&gt;를 Terraform으로 구현해보려 한다.
&lt;/p&gt;

&lt;p&gt;
Lambda는 서버를 직접 만들지 않고 코드를 실행할 수 있는 서비스다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;코드 작성
→ Lambda에 배포
→ 이벤트 발생
→ Lambda 실행&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
EventBridge는 특정 이벤트나 일정에 따라 Lambda를 실행할 수 있게 해준다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;정해진 시간
→ EventBridge Rule
→ Lambda 실행&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
예를 들어 매일 새벽 배치 작업을 실행하거나, 5분마다 상태 점검 작업을 수행할 수 있다.
&lt;/p&gt;

&lt;blockquote&gt;
  Lambda는 코드를 실행하는 리소스이고, EventBridge는 Lambda를 언제 실행할지 결정하는 트리거 역할을 한다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;목차&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;1. Lambda란 무엇인가&lt;/li&gt;
  &lt;li&gt;2. EventBridge란 무엇인가&lt;/li&gt;
  &lt;li&gt;3. Lambda를 만들 때 필요한 요소&lt;/li&gt;
  &lt;li&gt;4. Lambda 실행 Role 만들기&lt;/li&gt;
  &lt;li&gt;5. Lambda 코드 작성과 패키징&lt;/li&gt;
  &lt;li&gt;6. Lambda Function 만들기&lt;/li&gt;
  &lt;li&gt;7. CloudWatch Log Group 만들기&lt;/li&gt;
  &lt;li&gt;8. 환경 변수 설정하기&lt;/li&gt;
  &lt;li&gt;9. EventBridge Rule 만들기&lt;/li&gt;
  &lt;li&gt;10. EventBridge Target 만들기&lt;/li&gt;
  &lt;li&gt;11. Lambda Permission 설정하기&lt;/li&gt;
  &lt;li&gt;12. EventBridge Scheduler는 언제 사용할까?&lt;/li&gt;
  &lt;li&gt;13. 실전 예제: 5분마다 실행되는 Lambda&lt;/li&gt;
  &lt;li&gt;14. 의존성 흐름&lt;/li&gt;
  &lt;li&gt;15. 자주 하는 실수&lt;/li&gt;
  &lt;li&gt;16. 마무리&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2&gt;1. Lambda란 무엇인가&lt;/h2&gt;

&lt;p&gt;
Lambda는 AWS의 서버리스 컴퓨팅 서비스다.
&lt;/p&gt;

&lt;p&gt;
EC2처럼 서버를 직접 생성하고 운영하지 않아도, 코드를 업로드하면 AWS가 실행 환경을 관리해준다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EC2
→ 서버 생성
→ 런타임 설치
→ 애플리케이션 실행
→ 서버 관리 필요

Lambda
→ 코드 업로드
→ 이벤트 발생 시 실행
→ 서버 관리 부담 감소&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Lambda는 다음과 같은 작업에 자주 사용된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;스케줄 배치 작업
이미지 리사이징
S3 업로드 이벤트 처리
간단한 API 처리
EventBridge 이벤트 처리
알림 발송
운영 자동화 작업&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Lambda는 항상 실행 중인 서버가 아니라, 이벤트가 발생했을 때 실행되는 함수에 가깝다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;이벤트 발생
→ Lambda 실행
→ 작업 완료
→ 종료&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
따라서 짧게 실행되는 작업이나 이벤트 기반 작업에 적합하다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;2. EventBridge란 무엇인가&lt;/h2&gt;

&lt;p&gt;
EventBridge는 AWS의 이벤트 라우팅 서비스다.
&lt;/p&gt;

&lt;p&gt;
AWS 서비스, 사용자 애플리케이션, SaaS 서비스에서 발생한 이벤트를 받아 특정 대상에게 전달할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Event Source
→ EventBridge
→ Target&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Lambda와 함께 사용할 때는 보통 다음 두 가지 방식이 많다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;1. 일정 기반 실행
   예: 5분마다 Lambda 실행

2. 이벤트 기반 실행
   예: 특정 AWS 이벤트 발생 시 Lambda 실행&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이번 글에서는 초보자가 이해하기 쉬운 일정 기반 실행을 먼저 다룬다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;rate(5 minutes)
→ EventBridge Rule
→ Lambda&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
EventBridge에서 Lambda를 호출하려면 세 가지 리소스를 함께 생각해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EventBridge Rule
→ 언제 실행할지 정의

EventBridge Target
→ 무엇을 실행할지 정의

Lambda Permission
→ EventBridge가 Lambda를 호출할 수 있도록 허용&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;3. Lambda를 만들 때 필요한 요소&lt;/h2&gt;

&lt;p&gt;
Terraform으로 Lambda를 만들 때는 다음 요소가 필요하다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;구성 요소&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;Terraform 리소스&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;역할&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lambda Function&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_lambda_function&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;실행할 함수 본체&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;IAM Role&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_iam_role&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lambda가 사용할 실행 권한&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;IAM Policy Attachment&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_iam_role_policy_attachment&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;로그 출력 등 기본 권한 연결&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Code Package&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;archive_file&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lambda에 업로드할 zip 파일 생성&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Log Group&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_cloudwatch_log_group&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lambda 로그 저장과 보관 기간 관리&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;EventBridge Rule&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_cloudwatch_event_rule&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;실행 조건 또는 스케줄 정의&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;EventBridge Target&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_cloudwatch_event_target&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Rule이 호출할 대상 지정&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lambda Permission&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_lambda_permission&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;EventBridge가 Lambda를 호출하도록 허용&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
구조를 단순화하면 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lambda 코드
→ Lambda Function
→ EventBridge Rule
→ EventBridge Target
→ Lambda Permission&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;4. Lambda 실행 Role 만들기&lt;/h2&gt;

&lt;p&gt;
Lambda가 실행되려면 IAM Role이 필요하다.
&lt;/p&gt;

&lt;p&gt;
이 Role은 Lambda 서비스가 Assume할 수 있어야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lambda Service
→ AssumeRole
→ Lambda Execution Role&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Terraform 코드는 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;lambda&quot; {
  name = &quot;${var.project_name}-${var.environment}-lambda-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;lambda.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-lambda-role&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
여기서 중요한 부분은 다음이다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Principal = {
  Service = &quot;lambda.amazonaws.com&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 설정은 Lambda 서비스가 이 Role을 사용할 수 있다는 의미다.
&lt;/p&gt;

&lt;p&gt;
Lambda가 CloudWatch Logs에 로그를 남기려면 기본 실행 권한도 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_role_policy_attachment&quot; &quot;lambda_basic&quot; {
  role       = aws_iam_role.lambda.name
  policy_arn = &quot;arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 Managed Policy는 Lambda가 CloudWatch Logs에 로그를 쓸 수 있도록 해준다.
&lt;/p&gt;

&lt;blockquote&gt;
  Lambda 실행 Role은 Lambda가 실행 중에 사용할 AWS 권한의 기준이 된다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;5. Lambda 코드 작성과 패키징&lt;/h2&gt;

&lt;p&gt;
Lambda는 실행할 코드가 필요하다.
&lt;/p&gt;

&lt;p&gt;
예제로 간단한 Python Lambda 코드를 작성해보자.
&lt;/p&gt;

&lt;p&gt;
파일 구조는 다음과 같이 둘 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;project/
├── main.tf
├── variables.tf
├── outputs.tf
└── lambda_src/
    └── index.py&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
&lt;code&gt;lambda_src/index.py&lt;/code&gt; 파일 내용은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import json
import os
from datetime import datetime, timezone

def handler(event, context):
    app_env = os.environ.get(&quot;APP_ENV&quot;, &quot;dev&quot;)

    print(&quot;Lambda invoked&quot;)
    print(&quot;event:&quot;, json.dumps(event))

    return {
        &quot;statusCode&quot;: 200,
        &quot;body&quot;: json.dumps({
            &quot;message&quot;: &quot;hello from lambda&quot;,
            &quot;app_env&quot;: app_env,
            &quot;timestamp&quot;: datetime.now(timezone.utc).isoformat()
        })
    }&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Terraform에서는 이 파일을 zip으로 묶어서 Lambda에 업로드할 수 있다.
&lt;/p&gt;

&lt;p&gt;
이를 위해 &lt;code&gt;archive_file&lt;/code&gt; data source를 사용할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;data &quot;archive_file&quot; &quot;lambda&quot; {
  type        = &quot;zip&quot;
  source_dir  = &quot;${path.module}/lambda_src&quot;
  output_path = &quot;${path.module}/build/lambda.zip&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 설정은 &lt;code&gt;lambda_src&lt;/code&gt; 디렉토리를 zip으로 압축해서 &lt;code&gt;build/lambda.zip&lt;/code&gt; 파일을 만든다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;lambda_src/
→ build/lambda.zip&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
실제 운영에서는 애플리케이션 빌드와 패키징을 CI/CD에서 처리하는 경우가 많다.
&lt;/p&gt;

&lt;p&gt;
하지만 학습용이나 간단한 Lambda는 Terraform의 &lt;code&gt;archive_file&lt;/code&gt;로 시작해도 충분하다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;6. Lambda Function 만들기&lt;/h2&gt;

&lt;p&gt;
이제 Lambda Function을 만든다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_lambda_function&quot; &quot;app&quot; {
  function_name = &quot;${var.project_name}-${var.environment}-hello&quot;
  role          = aws_iam_role.lambda.arn

  runtime = &quot;python3.12&quot;
  handler = &quot;index.handler&quot;

  filename         = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256

  timeout     = 30
  memory_size = 128

  environment {
    variables = {
      APP_ENV = var.environment
    }
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-hello-lambda&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
주요 설정은 다음과 같다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;설정&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;의미&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;function_name&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lambda 함수 이름&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;role&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lambda 실행 Role ARN&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;runtime&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;실행 런타임&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;handler&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;실행할 함수 진입점&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;filename&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;업로드할 zip 파일 경로&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;source_code_hash&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;코드 변경 감지를 위한 해시&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;timeout&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;최대 실행 시간&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;memory_size&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;할당 메모리&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;code&gt;source_code_hash&lt;/code&gt;는 중요하다.
&lt;/p&gt;

&lt;p&gt;
Lambda zip 파일이 바뀌었을 때 Terraform이 코드 변경을 감지하는 데 사용된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;코드 변경
→ zip 변경
→ source_code_hash 변경
→ Lambda 코드 업데이트&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;7. CloudWatch Log Group 만들기&lt;/h2&gt;

&lt;p&gt;
Lambda에서 &lt;code&gt;print&lt;/code&gt;나 로그를 남기면 CloudWatch Logs로 전송된다.
&lt;/p&gt;

&lt;p&gt;
Lambda가 처음 실행될 때 Log Group이 자동으로 생길 수도 있지만, Terraform으로 직접 만들면 보관 기간을 관리하기 쉽다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_cloudwatch_log_group&quot; &quot;lambda&quot; {
  name              = &quot;/aws/lambda/${aws_lambda_function.app.function_name}&quot;
  retention_in_days = 14

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-lambda-log-group&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
&lt;code&gt;retention_in_days&lt;/code&gt;는 로그 보관 기간이다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;retention_in_days = 14
→ 로그를 14일 동안 보관&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
로그 보관 기간을 설정하지 않으면 로그가 계속 쌓여 비용이 증가할 수 있다.
&lt;/p&gt;

&lt;p&gt;
따라서 학습용이나 운영용 모두 Log Group의 보관 기간을 명시하는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;8. 환경 변수 설정하기&lt;/h2&gt;

&lt;p&gt;
Lambda에는 환경 변수를 넣을 수 있다.
&lt;/p&gt;

&lt;p&gt;
환경 변수는 코드에서 환경별 설정값을 읽을 때 유용하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;environment {
  variables = {
    APP_ENV = var.environment
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Python 코드에서는 다음처럼 읽을 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import os

app_env = os.environ.get(&quot;APP_ENV&quot;, &quot;dev&quot;)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
하지만 민감정보를 환경 변수에 직접 넣는 것은 주의해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;권장하지 않음:
DB_PASSWORD=my-password

권장:
DB_SECRET_NAME=/demo/prod/db/credential&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
비밀번호나 API Key 같은 값은 Secrets Manager나 SSM Parameter Store에 저장하고,
Lambda 환경 변수에는 Secret 이름이나 Parameter 이름만 넣는 방식이 더 안전하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lambda 환경 변수
→ Secret 이름 저장

Lambda 코드
→ Secrets Manager에서 실제 값 조회&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;9. EventBridge Rule 만들기&lt;/h2&gt;

&lt;p&gt;
EventBridge Rule은 Lambda를 언제 실행할지 정의한다.
&lt;/p&gt;

&lt;p&gt;
가장 간단한 예시는 5분마다 실행되는 스케줄이다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_cloudwatch_event_rule&quot; &quot;every_5_minutes&quot; {
  name        = &quot;${var.project_name}-${var.environment}-every-5-minutes&quot;
  description = &quot;Run Lambda every 5 minutes&quot;

  schedule_expression = &quot;rate(5 minutes)&quot;

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-every-5-minutes&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
&lt;code&gt;schedule_expression&lt;/code&gt;에는 &lt;code&gt;rate&lt;/code&gt; 또는 &lt;code&gt;cron&lt;/code&gt; 표현식을 사용할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;rate(5 minutes)
→ 5분마다 실행

rate(1 hour)
→ 1시간마다 실행

cron(0 18 * * ? *)
→ UTC 기준 매일 18:00 실행&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
주의할 점은 cron 표현식이 UTC 기준이라는 점이다.
&lt;/p&gt;

&lt;p&gt;
예를 들어 한국 시간 새벽 3시에 실행하려면 UTC로는 전날 18시를 고려해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;한국 시간 03:00
→ UTC 18:00&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;10. EventBridge Target 만들기&lt;/h2&gt;

&lt;p&gt;
Rule을 만들었다고 해서 Lambda가 자동으로 실행되는 것은 아니다.
&lt;/p&gt;

&lt;p&gt;
Rule이 호출할 대상을 지정해야 한다.
&lt;/p&gt;

&lt;p&gt;
이 역할을 하는 리소스가 EventBridge Target이다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_cloudwatch_event_target&quot; &quot;lambda&quot; {
  rule      = aws_cloudwatch_event_rule.every_5_minutes.name
  target_id = &quot;lambda&quot;
  arn       = aws_lambda_function.app.arn
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 코드는 다음 의미다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;every_5_minutes Rule이 실행될 때
→ aws_lambda_function.app Lambda를 호출한다.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
여기서 Target은 EventBridge Rule과 Lambda Function을 참조한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EventBridge Rule
→ EventBridge Target

Lambda Function
→ EventBridge Target&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
EventBridge Target에는 input 값을 전달할 수도 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_cloudwatch_event_target&quot; &quot;lambda&quot; {
  rule      = aws_cloudwatch_event_rule.every_5_minutes.name
  target_id = &quot;lambda&quot;
  arn       = aws_lambda_function.app.arn

  input = jsonencode({
    source = &quot;eventbridge&quot;
    job    = &quot;sample-scheduled-job&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이렇게 하면 Lambda의 event 인자로 해당 JSON이 전달된다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;11. Lambda Permission 설정하기&lt;/h2&gt;

&lt;p&gt;
EventBridge Target을 만들었다고 해서 EventBridge가 Lambda를 호출할 수 있는 것은 아니다.
&lt;/p&gt;

&lt;p&gt;
Lambda 쪽에서 EventBridge에게 호출 권한을 허용해야 한다.
&lt;/p&gt;

&lt;p&gt;
Terraform에서는 &lt;code&gt;aws_lambda_permission&lt;/code&gt;을 사용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_lambda_permission&quot; &quot;allow_eventbridge&quot; {
  statement_id  = &quot;AllowExecutionFromEventBridge&quot;
  action        = &quot;lambda:InvokeFunction&quot;
  function_name = aws_lambda_function.app.function_name
  principal     = &quot;events.amazonaws.com&quot;
  source_arn    = aws_cloudwatch_event_rule.every_5_minutes.arn
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 설정은 다음 의미다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EventBridge Rule이
이 Lambda Function을 호출할 수 있도록 허용한다.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
여기서 중요한 부분은 다음이다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;principal  = &quot;events.amazonaws.com&quot;
source_arn = aws_cloudwatch_event_rule.every_5_minutes.arn&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
&lt;code&gt;principal&lt;/code&gt;은 호출 주체가 EventBridge라는 뜻이고,
&lt;code&gt;source_arn&lt;/code&gt;은 어떤 EventBridge Rule에서 오는 호출을 허용할지 제한하는 값이다.
&lt;/p&gt;

&lt;blockquote&gt;
  EventBridge가 Lambda를 호출하려면 Target 설정뿐 아니라 Lambda Permission도 필요하다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;12. EventBridge Scheduler는 언제 사용할까?&lt;/h2&gt;

&lt;p&gt;
AWS에는 EventBridge Rule 외에도 &lt;strong&gt;EventBridge Scheduler&lt;/strong&gt;가 있다.
&lt;/p&gt;

&lt;p&gt;
특히 단순 스케줄 실행만 목적이라면 EventBridge Scheduler를 고려할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EventBridge Rule
→ 이벤트 패턴 또는 스케줄 기반 라우팅

EventBridge Scheduler
→ 일정 기반 실행에 특화된 스케줄러&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
AWS 문서에서도 기존 EventBridge scheduled rules 대신 EventBridge Scheduler 사용을 권장한다고 설명한다.
&lt;/p&gt;

&lt;p&gt;
다만 Terraform 초보 단계에서는 EventBridge Rule, Target, Lambda Permission의 관계를 먼저 이해하는 것이 좋다.
&lt;/p&gt;

&lt;p&gt;
이 구조를 이해하면 Scheduler를 사용할 때도 다음 질문을 쉽게 이해할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;누가 Lambda를 호출하는가?
어떤 권한으로 호출하는가?
언제 호출하는가?&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;13. 실전 예제: 5분마다 실행되는 Lambda&lt;/h2&gt;

&lt;p&gt;
이제 지금까지의 내용을 하나로 정리해보자.
&lt;/p&gt;

&lt;p&gt;
구조는 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EventBridge Rule
→ EventBridge Target
→ Lambda Permission
→ Lambda Function&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;13.1 variables.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;variable &quot;project_name&quot; {
  description = &quot;Project name&quot;
  type        = string
}

variable &quot;environment&quot; {
  description = &quot;Environment name&quot;
  type        = string
}

variable &quot;aws_region&quot; {
  description = &quot;AWS region&quot;
  type        = string
}

variable &quot;schedule_expression&quot; {
  description = &quot;EventBridge schedule expression&quot;
  type        = string
  default     = &quot;rate(5 minutes)&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;13.2 locals.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = &quot;terraform&quot;
  }

  function_name = &quot;${var.project_name}-${var.environment}-hello&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;13.3 lambda_src/index.py&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;import json
import os
from datetime import datetime, timezone

def handler(event, context):
    app_env = os.environ.get(&quot;APP_ENV&quot;, &quot;dev&quot;)

    print(&quot;Lambda invoked&quot;)
    print(&quot;event:&quot;, json.dumps(event))

    return {
        &quot;statusCode&quot;: 200,
        &quot;body&quot;: json.dumps({
            &quot;message&quot;: &quot;hello from lambda&quot;,
            &quot;app_env&quot;: app_env,
            &quot;timestamp&quot;: datetime.now(timezone.utc).isoformat()
        })
    }&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;13.4 main.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;data &quot;archive_file&quot; &quot;lambda&quot; {
  type        = &quot;zip&quot;
  source_dir  = &quot;${path.module}/lambda_src&quot;
  output_path = &quot;${path.module}/build/lambda.zip&quot;
}

resource &quot;aws_iam_role&quot; &quot;lambda&quot; {
  name = &quot;${local.function_name}-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;lambda.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${local.function_name}-role&quot;
  })
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;lambda_basic&quot; {
  role       = aws_iam_role.lambda.name
  policy_arn = &quot;arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole&quot;
}

resource &quot;aws_lambda_function&quot; &quot;app&quot; {
  function_name = local.function_name
  role          = aws_iam_role.lambda.arn

  runtime = &quot;python3.12&quot;
  handler = &quot;index.handler&quot;

  filename         = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256

  timeout     = 30
  memory_size = 128

  environment {
    variables = {
      APP_ENV = var.environment
    }
  }

  tags = merge(local.common_tags, {
    Name = local.function_name
  })
}

resource &quot;aws_cloudwatch_log_group&quot; &quot;lambda&quot; {
  name              = &quot;/aws/lambda/${aws_lambda_function.app.function_name}&quot;
  retention_in_days = 14

  tags = merge(local.common_tags, {
    Name = &quot;${local.function_name}-log-group&quot;
  })
}

resource &quot;aws_cloudwatch_event_rule&quot; &quot;schedule&quot; {
  name        = &quot;${local.function_name}-schedule&quot;
  description = &quot;Run Lambda by schedule&quot;

  schedule_expression = var.schedule_expression

  tags = merge(local.common_tags, {
    Name = &quot;${local.function_name}-schedule&quot;
  })
}

resource &quot;aws_cloudwatch_event_target&quot; &quot;lambda&quot; {
  rule      = aws_cloudwatch_event_rule.schedule.name
  target_id = &quot;lambda&quot;
  arn       = aws_lambda_function.app.arn

  input = jsonencode({
    source = &quot;eventbridge&quot;
    job    = &quot;sample-scheduled-job&quot;
  })
}

resource &quot;aws_lambda_permission&quot; &quot;allow_eventbridge&quot; {
  statement_id  = &quot;AllowExecutionFromEventBridge&quot;
  action        = &quot;lambda:InvokeFunction&quot;
  function_name = aws_lambda_function.app.function_name
  principal     = &quot;events.amazonaws.com&quot;
  source_arn    = aws_cloudwatch_event_rule.schedule.arn
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 구성의 특징은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lambda 실행 Role 생성
Lambda 기본 로그 권한 연결
Lambda 코드 zip 패키징
Lambda Function 생성
CloudWatch Log Group 생성
EventBridge Schedule Rule 생성
EventBridge Target으로 Lambda 연결
Lambda Permission으로 EventBridge 호출 허용&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;13.5 outputs.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;output &quot;lambda_function_name&quot; {
  description = &quot;Lambda function name&quot;
  value       = aws_lambda_function.app.function_name
}

output &quot;lambda_function_arn&quot; {
  description = &quot;Lambda function ARN&quot;
  value       = aws_lambda_function.app.arn
}

output &quot;eventbridge_rule_name&quot; {
  description = &quot;EventBridge rule name&quot;
  value       = aws_cloudwatch_event_rule.schedule.name
}

output &quot;eventbridge_rule_arn&quot; {
  description = &quot;EventBridge rule ARN&quot;
  value       = aws_cloudwatch_event_rule.schedule.arn
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;14. 의존성 흐름&lt;/h2&gt;

&lt;p&gt;
Lambda와 EventBridge를 Terraform으로 구현할 때의 의존성 흐름은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lambda Code / IAM Role
→ Lambda Function
→ EventBridge Target
→ Lambda Permission&lt;/code&gt;&lt;/pre&gt;

&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lDZGY/dJMcafGDy4E/97JcQTfcftK0J5LgxNayI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lDZGY/dJMcafGDy4E/97JcQTfcftK0J5LgxNayI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lDZGY/dJMcafGDy4E/97JcQTfcftK0J5LgxNayI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlDZGY%2FdJMcafGDy4E%2F97JcQTfcftK0J5LgxNayI0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;p&gt;
이 구조에서 중요한 점은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lambda Function은 코드 zip과 IAM Role을 참조한다.
EventBridge Target은 Rule과 Lambda Function을 참조한다.
Lambda Permission은 EventBridge Rule이 Lambda를 호출할 수 있도록 허용한다.&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
  EventBridge Target은 호출 대상을 지정하고, Lambda Permission은 그 호출을 Lambda가 허용하도록 만든다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;15. 자주 하는 실수&lt;/h2&gt;

&lt;h3&gt;15.1 Lambda Permission을 만들지 않음&lt;/h3&gt;

&lt;p&gt;
EventBridge Target을 만들었는데 Lambda가 실행되지 않는다면 Lambda Permission이 빠졌는지 확인해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EventBridge Target 있음
Lambda Permission 없음
→ EventBridge가 Lambda 호출 실패&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
EventBridge가 Lambda를 호출하려면 다음 권한이 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;principal  = &quot;events.amazonaws.com&quot;
action     = &quot;lambda:InvokeFunction&quot;
source_arn = EventBridge Rule ARN&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;15.2 Handler 이름을 잘못 설정함&lt;/h3&gt;

&lt;p&gt;
Python Lambda에서 &lt;code&gt;handler = &quot;index.handler&quot;&lt;/code&gt;라면 다음 의미다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;index.py 파일 안의
handler 함수 실행&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
파일명이나 함수명이 다르면 Lambda 실행 시 오류가 발생한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;handler = &quot;index.handler&quot;

필요한 코드:
index.py
def handler(event, context):&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;15.3 source_code_hash를 넣지 않음&lt;/h3&gt;

&lt;p&gt;
&lt;code&gt;source_code_hash&lt;/code&gt;를 넣지 않으면 코드 zip이 바뀌어도 Terraform이 변경을 제대로 감지하지 못할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;source_code_hash = data.archive_file.lambda.output_base64sha256&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Lambda 코드를 Terraform으로 직접 배포한다면 이 값을 넣는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;15.4 CloudWatch Log Group 보관 기간을 설정하지 않음&lt;/h3&gt;

&lt;p&gt;
Lambda 로그는 CloudWatch Logs에 쌓인다.
&lt;/p&gt;

&lt;p&gt;
보관 기간을 설정하지 않으면 로그가 계속 남아 비용이 증가할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_cloudwatch_log_group&quot; &quot;lambda&quot; {
  retention_in_days = 14
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;15.5 민감정보를 환경 변수에 직접 넣음&lt;/h3&gt;

&lt;p&gt;
Lambda 환경 변수에 DB password나 API key를 직접 넣는 것은 주의해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;권장하지 않음:
DB_PASSWORD=my-password

권장:
DB_SECRET_NAME=/demo/prod/db/credential&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
실제 민감정보는 Secrets Manager나 Parameter Store에 저장하고,
Lambda는 실행 Role 권한으로 값을 조회하는 구조가 더 안전하다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;15.6 cron 시간을 한국 시간으로 착각함&lt;/h3&gt;

&lt;p&gt;
EventBridge cron 표현식은 UTC 기준으로 해석된다.
&lt;/p&gt;

&lt;p&gt;
한국 시간 기준으로 실행 시간을 정할 때는 UTC 변환을 고려해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;한국 시간 03:00
→ UTC 18:00&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
시간 기반 작업은 운영에서 실수가 많기 때문에 주석이나 변수 설명에 기준 시간을 명확히 남기는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;15.7 EventBridge Rule과 Scheduler를 혼동함&lt;/h3&gt;

&lt;p&gt;
EventBridge Rule과 EventBridge Scheduler는 모두 일정 실행에 사용할 수 있지만 목적이 조금 다르다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EventBridge Rule
→ 이벤트 패턴과 스케줄 기반 라우팅에 사용

EventBridge Scheduler
→ 스케줄 실행에 특화된 서비스&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
스케줄 실행만 필요하다면 EventBridge Scheduler도 고려할 수 있다.
&lt;/p&gt;

&lt;p&gt;
다만 이 글에서는 Lambda, Rule, Target, Permission의 기본 관계를 이해하기 위해 EventBridge Rule 방식을 사용했다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;15.8 Terraform으로 복잡한 애플리케이션 빌드까지 처리하려고 함&lt;/h3&gt;

&lt;p&gt;
간단한 Lambda는 &lt;code&gt;archive_file&lt;/code&gt;로 패키징해도 된다.
&lt;/p&gt;

&lt;p&gt;
하지만 의존성이 많거나 빌드 과정이 필요한 Lambda는 CI/CD에서 빌드하고, Terraform은 배포 리소스 관리에 집중하는 것이 더 적합할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;간단한 예제:
Terraform archive_file 사용 가능

운영 애플리케이션:
CI/CD에서 빌드
Terraform은 Lambda 리소스 관리&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;16. 마무리&lt;/h2&gt;

&lt;p&gt;
이번 글에서는 Terraform으로 Lambda와 EventBridge를 구현하는 방법을 정리했다.
&lt;/p&gt;

&lt;p&gt;
Lambda는 서버를 직접 관리하지 않고 코드를 실행할 수 있는 서비스다.
&lt;/p&gt;

&lt;p&gt;
EventBridge는 일정이나 이벤트에 따라 Lambda를 실행할 수 있게 해준다.
&lt;/p&gt;

&lt;p&gt;
핵심 리소스는 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;aws_iam_role
→ Lambda 실행 Role

aws_lambda_function
→ Lambda 함수 본체

aws_cloudwatch_log_group
→ Lambda 로그 보관

aws_cloudwatch_event_rule
→ 실행 스케줄 또는 이벤트 조건

aws_cloudwatch_event_target
→ 호출 대상 Lambda 지정

aws_lambda_permission
→ EventBridge가 Lambda를 호출할 수 있도록 허용&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
처음에는 리소스가 많아 보이지만, 역할을 나누어 보면 단순하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lambda
→ 실행할 코드

EventBridge Rule
→ 언제 실행할지

EventBridge Target
→ 무엇을 실행할지

Lambda Permission
→ 호출을 허용할지&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3 style=&quot;text-align:center;&quot;&gt;한 줄 정리&lt;/h3&gt;

&lt;p style=&quot;text-align:center;&quot;&gt;
&lt;strong&gt;Lambda는 코드를 실행하는 리소스이고, EventBridge는 그 코드를 언제 실행할지 결정하는 트리거다.&lt;/strong&gt;
&lt;/p&gt;

&lt;hr /&gt;</description>
      <category>테라폼</category>
      <author>pininini</author>
      <guid isPermaLink="true">https://pininininfra.tistory.com/24</guid>
      <comments>https://pininininfra.tistory.com/24#entry24comment</comments>
      <pubDate>Fri, 15 May 2026 13:29:43 +0900</pubDate>
    </item>
    <item>
      <title>4-11. 테라폼 - ECS Fargate 기본 구현하기</title>
      <link>https://pininininfra.tistory.com/23</link>
      <description>&lt;h1 style=&quot;text-align:center;&quot;&gt;테라폼 - ECS Fargate 기본 구현하기&lt;/h1&gt;
&lt;p style=&quot;text-align:center;&quot;&gt;&lt;em&gt;ECR 이미지를 가져와 컨테이너를 실행하고 ALB와 연결하기&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;
이전 글에서는 Terraform으로 ECR을 구현하는 방법을 정리했다.
&lt;/p&gt;

&lt;p&gt;
이번 글에서는 ECR에 저장된 Docker 이미지를 실행하는 &lt;strong&gt;ECS Fargate&lt;/strong&gt;를 Terraform으로 구현해보려 한다.
&lt;/p&gt;

&lt;p&gt;
ECS는 Elastic Container Service의 약자다.
&lt;/p&gt;

&lt;p&gt;
AWS에서 컨테이너를 실행하고 관리하는 서비스이며, Fargate를 사용하면 EC2 서버를 직접 관리하지 않고 컨테이너를 실행할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECR
→ Docker Image 저장

ECS Fargate
→ Docker Image 실행

ALB
→ 외부 요청을 ECS Task로 전달&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ECS Fargate는 처음 보면 리소스가 많아 보여서 어렵게 느껴질 수 있다.
&lt;/p&gt;

&lt;p&gt;
하지만 핵심 구조는 단순하다.
&lt;/p&gt;

&lt;blockquote&gt;
  ECS Fargate는 ECR 이미지를 가져와 Task로 실행하고, Service가 원하는 개수만큼 Task를 유지하는 구조다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;목차&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;1. ECS Fargate란 무엇인가&lt;/li&gt;
  &lt;li&gt;2. ECS를 구성하는 주요 요소&lt;/li&gt;
  &lt;li&gt;3. ECS Cluster 만들기&lt;/li&gt;
  &lt;li&gt;4. Task Execution Role과 Task Role&lt;/li&gt;
  &lt;li&gt;5. CloudWatch Log Group 만들기&lt;/li&gt;
  &lt;li&gt;6. Task Definition 만들기&lt;/li&gt;
  &lt;li&gt;7. ECS Service 만들기&lt;/li&gt;
  &lt;li&gt;8. ALB Target Group과 연결하기&lt;/li&gt;
  &lt;li&gt;9. Security Group 구성&lt;/li&gt;
  &lt;li&gt;10. 환경 변수와 Secret 주입하기&lt;/li&gt;
  &lt;li&gt;11. desired_count와 배포 관리&lt;/li&gt;
  &lt;li&gt;12. 실전 예제: ALB → ECS Fargate 구조&lt;/li&gt;
  &lt;li&gt;13. 의존성 흐름&lt;/li&gt;
  &lt;li&gt;14. 자주 하는 실수&lt;/li&gt;
  &lt;li&gt;15. 마무리&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2&gt;1. ECS Fargate란 무엇인가&lt;/h2&gt;

&lt;p&gt;
ECS는 AWS에서 컨테이너를 실행하고 관리하는 서비스다.
&lt;/p&gt;

&lt;p&gt;
컨테이너를 실행하려면 원래 서버가 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EC2
→ Docker 설치
→ 컨테이너 실행&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
하지만 Fargate를 사용하면 EC2 인스턴스를 직접 만들고 관리하지 않아도 된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Docker Image
→ ECS Fargate
→ 컨테이너 실행&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
즉, Fargate는 서버 관리 부담을 줄여주는 컨테이너 실행 방식이다.
&lt;/p&gt;

&lt;p&gt;
ECS에는 크게 두 가지 실행 방식이 있다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;구분&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;설명&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;특징&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECS on EC2&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;EC2 인스턴스 위에서 컨테이너 실행&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;서버 관리 필요&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECS Fargate&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;서버 관리 없이 컨테이너 실행&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;초보자와 운영 단순화에 유리&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
이 글에서는 Fargate 기준으로 설명한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;2. ECS를 구성하는 주요 요소&lt;/h2&gt;

&lt;p&gt;
ECS Fargate를 이해하려면 다음 요소를 알아야 한다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;구성 요소&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;Terraform 리소스&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;역할&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Cluster&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_ecs_cluster&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECS 리소스를 묶는 논리적 공간&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Task Definition&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_ecs_task_definition&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;컨테이너 실행 설정&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Service&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_ecs_service&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Task를 원하는 개수만큼 유지&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Task Execution Role&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_iam_role&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECS가 이미지 pull, 로그 전송에 사용&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Task Role&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_iam_role&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;컨테이너 애플리케이션이 AWS API 호출에 사용&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Log Group&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_cloudwatch_log_group&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;컨테이너 로그 저장&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Security Group&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_security_group&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ALB와 ECS Task 간 접근 제어&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
구조를 단순화하면 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECR Image
→ Task Definition
→ ECS Service
→ ECS Task 실행&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
외부 요청까지 포함하면 다음 구조가 된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;User
→ ALB
→ Target Group
→ ECS Service
→ ECS Task&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;3. ECS Cluster 만들기&lt;/h2&gt;

&lt;p&gt;
ECS Cluster는 ECS 리소스를 묶는 논리적 공간이다.
&lt;/p&gt;

&lt;p&gt;
Fargate에서는 EC2 인스턴스를 직접 Cluster에 등록하지 않아도 된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecs_cluster&quot; &quot;app&quot; {
  name = &quot;${var.project_name}-${var.environment}-cluster&quot;

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-cluster&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Cluster 자체는 비교적 단순한 리소스다.
&lt;/p&gt;

&lt;p&gt;
실제 컨테이너 실행 설정은 Task Definition과 Service에서 결정된다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;4. Task Execution Role과 Task Role&lt;/h2&gt;

&lt;p&gt;
ECS를 처음 다룰 때 가장 헷갈리는 부분이 Role이다.
&lt;/p&gt;

&lt;p&gt;
ECS에서는 보통 두 종류의 Role을 구분한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Task Execution Role
Task Role&lt;/code&gt;&lt;/pre&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;Role&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;사용 주체&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;사용 예&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Task Execution Role&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECS / Fargate Agent&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECR 이미지 pull, CloudWatch Logs 전송, Secret 주입&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Task Role&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;컨테이너 안의 애플리케이션&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;S3 접근, Secrets Manager 조회, SQS 호출&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;blockquote&gt;
  Execution Role은 ECS가 쓰는 권한이고, Task Role은 애플리케이션이 쓰는 권한이다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3&gt;4.1 Task Execution Role&lt;/h3&gt;

&lt;p&gt;
Task Execution Role은 ECS가 Task를 실행하기 위해 사용하는 Role이다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;ecs_task_execution&quot; {
  name = &quot;${var.project_name}-${var.environment}-ecs-task-execution-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ecs-tasks.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-ecs-task-execution-role&quot;
  })
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;ecs_task_execution_default&quot; {
  role       = aws_iam_role.ecs_task_execution.name
  policy_arn = &quot;arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 AWS Managed Policy에는 기본적인 ECR pull, CloudWatch Logs 전송 권한이 포함된다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;4.2 Task Role&lt;/h3&gt;

&lt;p&gt;
Task Role은 컨테이너 내부의 애플리케이션이 사용하는 Role이다.
&lt;/p&gt;

&lt;p&gt;
예를 들어 애플리케이션이 S3를 읽거나 Secrets Manager 값을 직접 조회한다면 Task Role에 권한을 부여해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;ecs_task&quot; {
  name = &quot;${var.project_name}-${var.environment}-ecs-task-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ecs-tasks.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-ecs-task-role&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
처음에는 권한 없이 Role만 만들어두고, 이후 애플리케이션이 필요한 AWS API 권한만 추가해도 된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;예:
S3 읽기 필요
→ Task Role에 s3:GetObject 추가

Secrets Manager 조회 필요
→ Task Role에 secretsmanager:GetSecretValue 추가&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;5. CloudWatch Log Group 만들기&lt;/h2&gt;

&lt;p&gt;
ECS 컨테이너 로그는 CloudWatch Logs로 보낼 수 있다.
&lt;/p&gt;

&lt;p&gt;
이를 위해 Log Group을 먼저 만든다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_cloudwatch_log_group&quot; &quot;app&quot; {
  name              = &quot;/ecs/${var.project_name}/${var.environment}/app&quot;
  retention_in_days = 14

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-app-log-group&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
&lt;code&gt;retention_in_days&lt;/code&gt;는 로그 보관 기간이다.
&lt;/p&gt;

&lt;p&gt;
로그를 무제한으로 보관하면 비용이 계속 증가할 수 있으므로, 운영 정책에 맞게 보관 기간을 정하는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;학습용 / 개발용
→ 7일 또는 14일

운영
→ 30일, 90일, 180일 등 정책에 따라 설정&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;6. Task Definition 만들기&lt;/h2&gt;

&lt;p&gt;
Task Definition은 컨테이너 실행 설정이다.
&lt;/p&gt;

&lt;p&gt;
어떤 이미지를 사용할지, CPU와 메모리는 얼마인지, 어떤 포트를 열지, 로그는 어디로 보낼지 정의한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecs_task_definition&quot; &quot;app&quot; {
  family                   = &quot;${var.project_name}-${var.environment}-app&quot;
  requires_compatibilities = [&quot;FARGATE&quot;]
  network_mode             = &quot;awsvpc&quot;

  cpu    = 256
  memory = 512

  execution_role_arn = aws_iam_role.ecs_task_execution.arn
  task_role_arn      = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name  = &quot;app&quot;
      image = &quot;${var.ecr_repository_url}:${var.image_tag}&quot;

      essential = true

      portMappings = [
        {
          containerPort = 8080
          protocol      = &quot;tcp&quot;
        }
      ]

      environment = [
        {
          name  = &quot;APP_ENV&quot;
          value = var.environment
        }
      ]

      logConfiguration = {
        logDriver = &quot;awslogs&quot;
        options = {
          awslogs-group         = aws_cloudwatch_log_group.app.name
          awslogs-region        = var.aws_region
          awslogs-stream-prefix = &quot;ecs&quot;
        }
      }
    }
  ])

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-app-task-definition&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
주요 설정은 다음과 같다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;설정&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;의미&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;requires_compatibilities&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Fargate 사용 여부&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;network_mode&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Fargate에서는 awsvpc 사용&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;cpu / memory&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Task에 할당할 CPU와 메모리&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;execution_role_arn&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECS가 이미지 pull, 로그 전송에 사용할 Role&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;task_role_arn&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;컨테이너 애플리케이션이 사용할 Role&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;container_definitions&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;컨테이너 이미지, 포트, 환경 변수, 로그 설정&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
Fargate에서는 &lt;code&gt;network_mode = &quot;awsvpc&quot;&lt;/code&gt;를 사용한다.
&lt;/p&gt;

&lt;p&gt;
이 모드에서는 Task마다 ENI가 생성되고, Security Group과 Subnet이 Task에 직접 적용된다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;7. ECS Service 만들기&lt;/h2&gt;

&lt;p&gt;
Task Definition은 컨테이너 실행 방법을 정의한다.
&lt;/p&gt;

&lt;p&gt;
하지만 Task Definition만으로 컨테이너가 계속 실행되는 것은 아니다.
&lt;/p&gt;

&lt;p&gt;
ECS Service가 Task를 원하는 개수만큼 유지한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecs_service&quot; &quot;app&quot; {
  name            = &quot;${var.project_name}-${var.environment}-app-service&quot;
  cluster         = aws_ecs_cluster.app.id
  task_definition = aws_ecs_task_definition.app.arn

  desired_count = 2
  launch_type   = &quot;FARGATE&quot;

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.ecs_task.id]
    assign_public_ip = false
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-app-service&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
여기서 중요한 설정은 &lt;code&gt;desired_count&lt;/code&gt;다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;desired_count = 2&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 값은 ECS가 유지해야 하는 Task 개수다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;desired_count = 2
→ Task 2개 유지

Task 1개 장애 발생
→ ECS가 새 Task 실행&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Fargate Task를 Private Subnet에 배치하고 &lt;code&gt;assign_public_ip = false&lt;/code&gt;로 두면 외부에서 Task에 직접 접근하지 않는다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Internet
→ ALB
→ ECS Task&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;8. ALB Target Group과 연결하기&lt;/h2&gt;

&lt;p&gt;
웹 서비스로 ECS를 운영하려면 ALB와 연결하는 경우가 많다.
&lt;/p&gt;

&lt;p&gt;
ECS Service의 &lt;code&gt;load_balancer&lt;/code&gt; 블록에서 Target Group을 연결한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecs_service&quot; &quot;app&quot; {
  name            = &quot;${var.project_name}-${var.environment}-app-service&quot;
  cluster         = aws_ecs_cluster.app.id
  task_definition = aws_ecs_task_definition.app.arn

  desired_count = 2
  launch_type   = &quot;FARGATE&quot;

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.ecs_task.id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = var.target_group_arn
    container_name   = &quot;app&quot;
    container_port   = 8080
  }

  depends_on = [
    var.listener_dependency
  ]

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-app-service&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
위 코드에서 &lt;code&gt;load_balancer&lt;/code&gt;는 다음 의미다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Target Group
→ ECS Service의 Task로 트래픽 전달

container_name
→ Task Definition의 컨테이너 이름

container_port
→ 컨테이너가 실제로 listen하는 포트&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
중요한 점은 Fargate에서는 Target Group의 &lt;code&gt;target_type&lt;/code&gt;을 보통 &lt;code&gt;ip&lt;/code&gt;로 둔다는 것이다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EC2 Target Group
→ target_type = &quot;instance&quot;

ECS Fargate Target Group
→ target_type = &quot;ip&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ECS Fargate Task는 EC2 Instance ID가 아니라 Task ENI의 IP로 Target Group에 등록되기 때문이다.
&lt;/p&gt;

&lt;p&gt;
참고로 위 예시의 &lt;code&gt;depends_on = [var.listener_dependency]&lt;/code&gt;는 실제 Terraform 코드에서는 변수로 직접 쓰기 어렵다.
&lt;/p&gt;

&lt;p&gt;
같은 코드 안에서 ALB Listener를 함께 만든다면 다음처럼 명시할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;depends_on = [
  aws_lb_listener.http
]&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
만약 ALB를 별도 모듈이나 별도 state에서 관리한다면, 이미 Listener와 Target Group이 생성된 상태에서 ECS Service를 적용하는 방식으로 나누는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;9. Security Group 구성&lt;/h2&gt;

&lt;p&gt;
ECS Fargate에서는 Task마다 네트워크 인터페이스가 생긴다.
&lt;/p&gt;

&lt;p&gt;
따라서 Task에 Security Group을 직접 연결한다.
&lt;/p&gt;

&lt;p&gt;
보통 구조는 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ALB Security Group
→ ECS Task Security Group
→ 8080 허용&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Terraform 코드는 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;ecs_task&quot; {
  name        = &quot;${var.project_name}-${var.environment}-ecs-task-sg&quot;
  description = &quot;Security group for ECS Fargate task&quot;
  vpc_id      = var.vpc_id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-ecs-task-sg&quot;
  })
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;ecs_from_alb&quot; {
  security_group_id = aws_security_group.ecs_task.id

  referenced_security_group_id = var.alb_security_group_id
  ip_protocol                  = &quot;tcp&quot;
  from_port                    = 8080
  to_port                      = 8080

  description = &quot;Allow app traffic from ALB&quot;
}

resource &quot;aws_vpc_security_group_egress_rule&quot; &quot;ecs_all&quot; {
  security_group_id = aws_security_group.ecs_task.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;-1&quot;

  description = &quot;Allow all outbound traffic&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이렇게 하면 ECS Task는 외부 전체가 아니라 ALB에서 오는 요청만 받을 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;권장 구조:
Internet
→ ALB
→ ECS Task

피하고 싶은 구조:
Internet
→ ECS Task 직접 접근&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;10. 환경 변수와 Secret 주입하기&lt;/h2&gt;

&lt;p&gt;
ECS Task Definition에서는 일반 환경 변수와 Secret 값을 구분해서 넣을 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;environment
→ 일반 설정값

secrets
→ Secrets Manager / SSM Parameter Store 값&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
예를 들어 일반 환경 변수는 다음처럼 작성한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;environment = [
  {
    name  = &quot;APP_ENV&quot;
    value = var.environment
  },
  {
    name  = &quot;LOG_LEVEL&quot;
    value = &quot;info&quot;
  }
]&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Secret 값은 &lt;code&gt;secrets&lt;/code&gt; 블록에 넣는다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;secrets = [
  {
    name      = &quot;DB_CREDENTIAL&quot;
    valueFrom = var.db_secret_arn
  },
  {
    name      = &quot;API_TOKEN&quot;
    valueFrom = var.api_token_parameter_arn
  }
]&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 설정을 Task Definition 안에 포함하면 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;container_definitions = jsonencode([
  {
    name  = &quot;app&quot;
    image = &quot;${var.ecr_repository_url}:${var.image_tag}&quot;

    essential = true

    portMappings = [
      {
        containerPort = 8080
        protocol      = &quot;tcp&quot;
      }
    ]

    environment = [
      {
        name  = &quot;APP_ENV&quot;
        value = var.environment
      }
    ]

    secrets = [
      {
        name      = &quot;DB_CREDENTIAL&quot;
        valueFrom = var.db_secret_arn
      }
    ]

    logConfiguration = {
      logDriver = &quot;awslogs&quot;
      options = {
        awslogs-group         = aws_cloudwatch_log_group.app.name
        awslogs-region        = var.aws_region
        awslogs-stream-prefix = &quot;ecs&quot;
      }
    }
  }
])&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Secret을 주입하려면 Task Execution Role에 해당 Secret이나 Parameter를 읽을 권한이 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Secrets Manager
→ secretsmanager:GetSecretValue

SSM Parameter Store
→ ssm:GetParameter

KMS 사용 시
→ kms:Decrypt&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;11. desired_count와 배포 관리&lt;/h2&gt;

&lt;p&gt;
&lt;code&gt;desired_count&lt;/code&gt;는 ECS Service가 유지할 Task 개수다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;desired_count = 2&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 값이 2라면 ECS는 정상 Task 2개를 유지하려고 한다.
&lt;/p&gt;

&lt;p&gt;
하나의 Task가 비정상 상태가 되면 새 Task를 실행해서 개수를 맞춘다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Task 1개 장애
→ ECS가 새 Task 실행
→ desired_count 유지&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 운영에서는 &lt;code&gt;desired_count&lt;/code&gt;를 Terraform이 계속 관리할지 고민해야 한다.
&lt;/p&gt;

&lt;p&gt;
예를 들어 Auto Scaling이나 배포 파이프라인이 ECS Service의 desired count를 변경할 수 있다.
&lt;/p&gt;

&lt;p&gt;
그런데 Terraform 코드에 &lt;code&gt;desired_count = 2&lt;/code&gt;가 고정되어 있으면, 다음 apply 때 다시 2로 되돌리려 할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Auto Scaling
→ desired_count = 4

Terraform apply
→ desired_count = 2로 되돌리려 함&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이런 경우에는 lifecycle의 &lt;code&gt;ignore_changes&lt;/code&gt;를 고려할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecs_service&quot; &quot;app&quot; {
  name            = &quot;${var.project_name}-${var.environment}-app-service&quot;
  cluster         = aws_ecs_cluster.app.id
  task_definition = aws_ecs_task_definition.app.arn

  desired_count = 2

  lifecycle {
    ignore_changes = [
      desired_count
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 초보 단계에서는 먼저 Terraform이 &lt;code&gt;desired_count&lt;/code&gt;를 관리하는 방식으로 이해하는 것이 좋다.
&lt;/p&gt;

&lt;p&gt;
Auto Scaling이나 배포 자동화를 도입한 뒤에 &lt;code&gt;ignore_changes&lt;/code&gt;를 적용할지 판단하면 된다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;12. 실전 예제: ALB → ECS Fargate 구조&lt;/h2&gt;

&lt;p&gt;
이제 지금까지의 내용을 하나로 합쳐보자.
&lt;/p&gt;

&lt;p&gt;
구조는 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Internet
→ ALB
→ Target Group
→ ECS Service
→ ECS Fargate Task
→ ECR Image&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;12.1 variables.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;variable &quot;project_name&quot; {
  description = &quot;Project name&quot;
  type        = string
}

variable &quot;environment&quot; {
  description = &quot;Environment name&quot;
  type        = string
}

variable &quot;aws_region&quot; {
  description = &quot;AWS region&quot;
  type        = string
}

variable &quot;vpc_id&quot; {
  description = &quot;VPC ID&quot;
  type        = string
}

variable &quot;private_subnet_ids&quot; {
  description = &quot;Private subnet IDs for ECS tasks&quot;
  type        = list(string)
}

variable &quot;alb_security_group_id&quot; {
  description = &quot;ALB security group ID&quot;
  type        = string
}

variable &quot;target_group_arn&quot; {
  description = &quot;ALB target group ARN&quot;
  type        = string
}

variable &quot;ecr_repository_url&quot; {
  description = &quot;ECR repository URL&quot;
  type        = string
}

variable &quot;image_tag&quot; {
  description = &quot;Docker image tag&quot;
  type        = string
  default     = &quot;latest&quot;
}

variable &quot;desired_count&quot; {
  description = &quot;Desired ECS task count&quot;
  type        = number
  default     = 2
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;12.2 locals.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = &quot;terraform&quot;
  }

  app_name = &quot;${var.project_name}-${var.environment}-app&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;12.3 main.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecs_cluster&quot; &quot;app&quot; {
  name = &quot;${var.project_name}-${var.environment}-cluster&quot;

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-cluster&quot;
  })
}

resource &quot;aws_cloudwatch_log_group&quot; &quot;app&quot; {
  name              = &quot;/ecs/${var.project_name}/${var.environment}/app&quot;
  retention_in_days = 14

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-app-log-group&quot;
  })
}

resource &quot;aws_iam_role&quot; &quot;ecs_task_execution&quot; {
  name = &quot;${var.project_name}-${var.environment}-ecs-task-execution-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ecs-tasks.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-ecs-task-execution-role&quot;
  })
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;ecs_task_execution_default&quot; {
  role       = aws_iam_role.ecs_task_execution.name
  policy_arn = &quot;arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy&quot;
}

resource &quot;aws_iam_role&quot; &quot;ecs_task&quot; {
  name = &quot;${var.project_name}-${var.environment}-ecs-task-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ecs-tasks.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-ecs-task-role&quot;
  })
}

resource &quot;aws_security_group&quot; &quot;ecs_task&quot; {
  name        = &quot;${var.project_name}-${var.environment}-ecs-task-sg&quot;
  description = &quot;Security group for ECS Fargate task&quot;
  vpc_id      = var.vpc_id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-ecs-task-sg&quot;
  })
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;ecs_from_alb&quot; {
  security_group_id = aws_security_group.ecs_task.id

  referenced_security_group_id = var.alb_security_group_id
  ip_protocol                  = &quot;tcp&quot;
  from_port                    = 8080
  to_port                      = 8080

  description = &quot;Allow traffic from ALB&quot;
}

resource &quot;aws_vpc_security_group_egress_rule&quot; &quot;ecs_all&quot; {
  security_group_id = aws_security_group.ecs_task.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;-1&quot;

  description = &quot;Allow all outbound traffic&quot;
}

resource &quot;aws_ecs_task_definition&quot; &quot;app&quot; {
  family                   = local.app_name
  requires_compatibilities = [&quot;FARGATE&quot;]
  network_mode             = &quot;awsvpc&quot;

  cpu    = 256
  memory = 512

  execution_role_arn = aws_iam_role.ecs_task_execution.arn
  task_role_arn      = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name  = &quot;app&quot;
      image = &quot;${var.ecr_repository_url}:${var.image_tag}&quot;

      essential = true

      portMappings = [
        {
          containerPort = 8080
          protocol      = &quot;tcp&quot;
        }
      ]

      environment = [
        {
          name  = &quot;APP_ENV&quot;
          value = var.environment
        }
      ]

      logConfiguration = {
        logDriver = &quot;awslogs&quot;
        options = {
          awslogs-group         = aws_cloudwatch_log_group.app.name
          awslogs-region        = var.aws_region
          awslogs-stream-prefix = &quot;ecs&quot;
        }
      }
    }
  ])

  tags = merge(local.common_tags, {
    Name = &quot;${local.app_name}-task-definition&quot;
  })
}

resource &quot;aws_ecs_service&quot; &quot;app&quot; {
  name            = &quot;${local.app_name}-service&quot;
  cluster         = aws_ecs_cluster.app.id
  task_definition = aws_ecs_task_definition.app.arn

  desired_count = var.desired_count
  launch_type   = &quot;FARGATE&quot;

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.ecs_task.id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = var.target_group_arn
    container_name   = &quot;app&quot;
    container_port   = 8080
  }

  tags = merge(local.common_tags, {
    Name = &quot;${local.app_name}-service&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;12.4 outputs.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;output &quot;ecs_cluster_name&quot; {
  description = &quot;ECS cluster name&quot;
  value       = aws_ecs_cluster.app.name
}

output &quot;ecs_service_name&quot; {
  description = &quot;ECS service name&quot;
  value       = aws_ecs_service.app.name
}

output &quot;ecs_task_definition_arn&quot; {
  description = &quot;ECS task definition ARN&quot;
  value       = aws_ecs_task_definition.app.arn
}

output &quot;ecs_task_security_group_id&quot; {
  description = &quot;ECS task security group ID&quot;
  value       = aws_security_group.ecs_task.id
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 구성의 특징은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECS Cluster 생성
Task Execution Role 생성
Task Role 생성
CloudWatch Log Group 생성
ECS Task Security Group 생성
Task Definition 생성
ECS Service 생성
ALB Target Group과 연결
ECR Image 실행&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;13. 의존성 흐름&lt;/h2&gt;

&lt;p&gt;
ECS Fargate를 Terraform으로 구현할 때의 의존성 흐름은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECR / IAM / Log Group / Security Group
→ Task Definition
→ ECS Service
→ ALB Target Group&lt;/code&gt;&lt;/pre&gt;

&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VxTls/dJMcagMjFd6/bkZw8nsBf7wOWd5II3hi1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VxTls/dJMcagMjFd6/bkZw8nsBf7wOWd5II3hi1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VxTls/dJMcagMjFd6/bkZw8nsBf7wOWd5II3hi1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVxTls%2FdJMcagMjFd6%2FbkZw8nsBf7wOWd5II3hi1K%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;p&gt;
이 구조에서 중요한 점은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Task Definition은 ECR Image, IAM Role, Log Group을 참조한다.
ECS Service는 Cluster, Task Definition, Security Group, Target Group을 참조한다.
ALB Target Group은 ECS Service를 통해 실행된 Task로 트래픽을 전달한다.&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
  ECS Service는 Task Definition을 실행하고, ALB Target Group과 연결되어 외부 요청을 Task로 전달한다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;14. 자주 하는 실수&lt;/h2&gt;

&lt;h3&gt;14.1 Fargate에서 network_mode를 awsvpc로 설정하지 않음&lt;/h3&gt;

&lt;p&gt;
Fargate에서는 &lt;code&gt;network_mode = &quot;awsvpc&quot;&lt;/code&gt;를 사용해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;network_mode = &quot;awsvpc&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 설정을 통해 Task마다 ENI가 생성되고, Subnet과 Security Group이 Task에 직접 적용된다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.2 ECS Fargate Target Group을 instance 타입으로 만듦&lt;/h3&gt;

&lt;p&gt;
ECS Fargate는 보통 Target Group의 &lt;code&gt;target_type&lt;/code&gt;을 &lt;code&gt;ip&lt;/code&gt;로 사용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EC2 대상
→ target_type = &quot;instance&quot;

ECS Fargate 대상
→ target_type = &quot;ip&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Fargate Task는 EC2 Instance ID가 아니라 Task ENI의 IP로 Target Group에 등록되기 때문이다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.3 Execution Role과 Task Role을 헷갈림&lt;/h3&gt;

&lt;p&gt;
Execution Role과 Task Role은 용도가 다르다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Execution Role
→ ECS가 이미지 pull, 로그 전송, secret 주입에 사용

Task Role
→ 컨테이너 안의 애플리케이션이 AWS API 호출에 사용&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
애플리케이션이 S3나 Secrets Manager를 직접 호출한다면 Task Role에 권한을 줘야 한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.4 ECS Task Security Group에서 ALB 접근을 허용하지 않음&lt;/h3&gt;

&lt;p&gt;
ALB가 ECS Task에 접근하려면 ECS Task Security Group에서 ALB Security Group을 허용해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ALB Security Group
→ ECS Task Security Group 8080 허용&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 설정이 없으면 ALB는 정상적으로 요청을 받아도 ECS Task로 전달하지 못한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.5 Health Check 경로가 애플리케이션과 맞지 않음&lt;/h3&gt;

&lt;p&gt;
Target Group의 Health Check 경로와 애플리케이션의 실제 health endpoint가 맞아야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Target Group health_check path = &quot;/health&quot;

Application endpoint 없음
→ Target unhealthy&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Spring Boot Actuator를 사용한다면 다음처럼 맞출 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;health_check path = &quot;/actuator/health&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.6 Private Subnet에 Task를 두고 NAT Gateway나 VPC Endpoint를 고려하지 않음&lt;/h3&gt;

&lt;p&gt;
Private Subnet에 ECS Task를 두고 &lt;code&gt;assign_public_ip = false&lt;/code&gt;로 설정하면 Task는 인터넷에 직접 나갈 수 없다.
&lt;/p&gt;

&lt;p&gt;
하지만 ECR 이미지 pull, CloudWatch Logs 전송, 외부 API 호출 등이 필요할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECS Task
→ ECR pull
→ CloudWatch Logs 전송
→ 외부 API 호출&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 경우 NAT Gateway나 VPC Endpoint를 고려해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Private ECS Task
→ NAT Gateway
→ Internet

또는

Private ECS Task
→ VPC Endpoint
→ ECR / CloudWatch Logs&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.7 latest 태그만 사용함&lt;/h3&gt;

&lt;p&gt;
ECS Task Definition에서 &lt;code&gt;latest&lt;/code&gt; 태그만 사용하면 어떤 이미지가 배포되었는지 추적하기 어렵다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;비추천:
app:latest

추천:
app:git-sha-abc1234
app:v1.0.0&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
운영에서는 Git SHA나 버전 번호처럼 고유한 image tag를 사용하는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.8 Terraform apply만으로 새 이미지가 자동 배포된다고 생각함&lt;/h3&gt;

&lt;p&gt;
ECR에 새 이미지를 push했다고 해서 ECS Service가 자동으로 새 이미지를 실행하는 것은 아니다.
&lt;/p&gt;

&lt;p&gt;
Task Definition의 image tag가 바뀌거나, ECS Service 배포가 트리거되어야 새 Task가 실행된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;docker push
→ ECR 이미지 저장

ECS 새 배포
→ 새 이미지로 Task 실행&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 흐름은 이후 CI/CD 시리즈에서 자세히 다루는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.9 desired_count를 Terraform과 Auto Scaling이 동시에 관리함&lt;/h3&gt;

&lt;p&gt;
Auto Scaling이 desired_count를 변경하는 구조라면 Terraform이 그 값을 다시 되돌리려 할 수 있다.
&lt;/p&gt;

&lt;p&gt;
이 경우 필요에 따라 &lt;code&gt;ignore_changes&lt;/code&gt;를 고려한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;lifecycle {
  ignore_changes = [
    desired_count
  ]
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 처음에는 Terraform이 desired_count를 관리하도록 두고, Auto Scaling을 도입할 때 별도로 조정하는 것이 이해하기 쉽다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;15. 마무리&lt;/h2&gt;

&lt;p&gt;
이번 글에서는 Terraform으로 ECS Fargate를 구현하는 방법을 정리했다.
&lt;/p&gt;

&lt;p&gt;
ECS Fargate는 여러 리소스가 함께 연결되어 동작한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECR
→ 이미지 저장

Task Definition
→ 컨테이너 실행 설정

ECS Service
→ Task 개수 유지

ALB Target Group
→ 외부 트래픽 전달

Security Group
→ ALB와 Task 간 접근 제어

IAM Role
→ 이미지 pull, 로그 전송, AWS API 접근 권한&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
처음에는 리소스가 많아 보이지만, 역할을 나누어 보면 구조는 명확하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;이미지는 ECR에 저장한다.
실행 방법은 Task Definition에 정의한다.
실행 상태는 ECS Service가 유지한다.
외부 요청은 ALB Target Group을 통해 들어온다.&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3 style=&quot;text-align:center;&quot;&gt;한 줄 정리&lt;/h3&gt;

&lt;p style=&quot;text-align:center;&quot;&gt;
&lt;strong&gt;ECS Fargate는 ECR 이미지를 Task Definition으로 실행하고, ECS Service가 원하는 개수만큼 유지하며, ALB를 통해 외부 요청을 전달받는 구조다.&lt;/strong&gt;
&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;
다음 글에서는 Lambda와 EventBridge를 Terraform으로 구현해본다.
Lambda는 서버를 직접 관리하지 않고 코드를 실행하는 서비스이고, EventBridge를 사용하면 정해진 시간이나 이벤트에 따라 Lambda를 실행할 수 있다.
&lt;/p&gt;</description>
      <category>테라폼</category>
      <author>pininini</author>
      <guid isPermaLink="true">https://pininininfra.tistory.com/23</guid>
      <comments>https://pininininfra.tistory.com/23#entry23comment</comments>
      <pubDate>Thu, 14 May 2026 19:52:31 +0900</pubDate>
    </item>
    <item>
      <title>4-10. 테라폼 - ECR 구현하기</title>
      <link>https://pininininfra.tistory.com/22</link>
      <description>&lt;h1 style=&quot;text-align:center;&quot;&gt;테라폼 - ECR 구현하기&lt;/h1&gt;
&lt;p style=&quot;text-align:center;&quot;&gt;&lt;em&gt;Docker 이미지를 저장할 컨테이너 이미지 레지스트리 만들기&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;
이전 글에서는 Terraform으로 ALB와 Target Group을 구현하는 방법을 정리했다.
&lt;/p&gt;

&lt;p&gt;
이번 글에서는 AWS에서 Docker 이미지를 저장할 때 사용하는 &lt;strong&gt;ECR&lt;/strong&gt;을 Terraform으로 구현해보려 한다.
&lt;/p&gt;

&lt;p&gt;
ECR은 Elastic Container Registry의 약자다.
&lt;/p&gt;

&lt;p&gt;
쉽게 말하면 AWS에서 제공하는 Docker 이미지 저장소다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Docker Image
→ ECR Repository
→ ECS / Lambda / EC2에서 사용&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ECR은 ECS Fargate, Lambda Container Image, EC2 기반 Docker 배포에서 자주 사용된다.
&lt;/p&gt;

&lt;p&gt;
다만 이 글에서는 Docker 이미지를 빌드하고 push하는 배포 흐름까지 깊게 다루지는 않는다.
&lt;/p&gt;

&lt;blockquote&gt;
  Terraform은 ECR 저장소를 만들고, 이미지를 빌드하고 push하는 것은 CI/CD가 담당하는 영역으로 나누어 생각하는 것이 좋다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;목차&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;1. ECR이란 무엇인가&lt;/li&gt;
  &lt;li&gt;2. ECR은 어디에 사용될까?&lt;/li&gt;
  &lt;li&gt;3. ECR과 배포의 역할 분리&lt;/li&gt;
  &lt;li&gt;4. ECR을 만들 때 고려할 설정&lt;/li&gt;
  &lt;li&gt;5. 기본 ECR Repository 만들기&lt;/li&gt;
  &lt;li&gt;6. Image Tag Mutability 설정&lt;/li&gt;
  &lt;li&gt;7. Image Scanning 설정&lt;/li&gt;
  &lt;li&gt;8. Encryption 설정&lt;/li&gt;
  &lt;li&gt;9. Lifecycle Policy 설정&lt;/li&gt;
  &lt;li&gt;10. Repository Policy와 IAM 권한&lt;/li&gt;
  &lt;li&gt;11. ECS에서 사용할 Repository URL 출력하기&lt;/li&gt;
  &lt;li&gt;12. 실전 예제: 애플리케이션용 ECR 구성&lt;/li&gt;
  &lt;li&gt;13. 의존성 흐름&lt;/li&gt;
  &lt;li&gt;14. 자주 하는 실수&lt;/li&gt;
  &lt;li&gt;15. 마무리&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2&gt;1. ECR이란 무엇인가&lt;/h2&gt;

&lt;p&gt;
ECR은 Elastic Container Registry의 약자다.
&lt;/p&gt;

&lt;p&gt;
컨테이너 이미지를 저장하고 관리하는 AWS의 이미지 레지스트리 서비스다.
&lt;/p&gt;

&lt;p&gt;
로컬에서 Docker 이미지를 만들면 보통 다음 흐름으로 사용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Dockerfile
→ docker build
→ Docker Image
→ docker push
→ Image Registry&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
여기서 AWS 환경에서 사용하는 이미지 저장소가 ECR이다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Docker Image
→ Amazon ECR
→ ECS / Lambda / EC2&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
예를 들어 Spring Boot 애플리케이션을 Docker 이미지로 만들었다면,
그 이미지를 ECR에 push하고 ECS Fargate에서 해당 이미지를 pull해서 실행할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Spring Boot App
→ Docker Image
→ ECR
→ ECS Fargate Task&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;2. ECR은 어디에 사용될까?&lt;/h2&gt;

&lt;p&gt;
ECR은 컨테이너 기반 서비스를 사용할 때 자주 등장한다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;사용처&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;설명&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECS Fargate&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Task Definition에서 ECR 이미지 URL을 사용&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECS EC2&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;EC2 기반 ECS Cluster에서 ECR 이미지를 pull&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lambda Container Image&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lambda 함수를 컨테이너 이미지로 배포할 때 사용&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;EC2 Docker 배포&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;EC2 내부에서 docker pull로 이미지 실행&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;CI/CD&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;GitHub Actions, CodeBuild 등이 이미지를 push&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
ECR 자체는 애플리케이션을 실행하지 않는다.
&lt;/p&gt;

&lt;p&gt;
ECR은 이미지를 저장하고, ECS나 Lambda 같은 실행 환경이 그 이미지를 가져가서 실행한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECR
→ 이미지 저장소

ECS / Lambda / EC2
→ 이미지 실행 환경&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;3. ECR과 배포의 역할 분리&lt;/h2&gt;

&lt;p&gt;
ECR 글에서 헷갈리기 쉬운 부분이 있다.
&lt;/p&gt;

&lt;p&gt;
바로 ECR 구현과 Docker 이미지 배포를 섞어서 생각하는 것이다.
&lt;/p&gt;

&lt;p&gt;
Terraform이 담당하기 좋은 부분은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECR Repository 생성
Lifecycle Policy 설정
Image Scanning 설정
Tag Mutability 설정
Repository Policy 설정
Repository URL output&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
반면 CI/CD가 담당하기 좋은 부분은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;docker build
docker tag
aws ecr get-login-password
docker push
ECS service update
배포 후 health check&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
따라서 이 글에서는 ECR 리소스를 Terraform으로 만드는 데 집중한다.
&lt;/p&gt;

&lt;p&gt;
이미지를 빌드하고 ECR에 push하는 흐름은 이후 GitHub Actions 또는 CI/CD 시리즈에서 따로 다루는 것이 자연스럽다.
&lt;/p&gt;

&lt;blockquote&gt;
  ECR 저장소 생성은 Terraform 영역이고, 이미지 push와 서비스 배포는 CI/CD 영역으로 나누는 것이 좋다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;4. ECR을 만들 때 고려할 설정&lt;/h2&gt;

&lt;p&gt;
ECR Repository를 만들 때는 단순히 저장소 이름만 정하는 것으로 끝나지 않는다.
&lt;/p&gt;

&lt;p&gt;
다음 설정을 함께 고려하는 것이 좋다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;설정&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;Terraform 속성 / 리소스&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;역할&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Repository&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_ecr_repository&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECR 저장소 본체&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Tag Mutability&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;image_tag_mutability&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;같은 태그 덮어쓰기 허용 여부&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Image Scan&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;image_scanning_configuration&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;이미지 취약점 스캔&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Encryption&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;encryption_configuration&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;이미지 저장 암호화&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lifecycle Policy&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_ecr_lifecycle_policy&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;오래된 이미지 자동 정리&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Repository Policy&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_ecr_repository_policy&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;다른 계정이나 특정 주체 접근 허용&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
초보자라면 먼저 다음 구성을 기본값으로 생각하면 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Private ECR Repository
scan_on_push = true
image_tag_mutability = &quot;IMMUTABLE&quot;
Lifecycle Policy 설정
repository_url output 생성&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;5. 기본 ECR Repository 만들기&lt;/h2&gt;

&lt;p&gt;
가장 기본적인 ECR Repository는 다음처럼 만들 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecr_repository&quot; &quot;app&quot; {
  name = &quot;${var.project_name}/${var.environment}/app&quot;

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-app-ecr&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ECR Repository 이름은 슬래시를 포함할 수 있다.
&lt;/p&gt;

&lt;p&gt;
예를 들어 다음처럼 환경과 서비스 이름을 포함해서 정리할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;demo/dev/app
demo/prod/app
demo/prod/admin&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이렇게 이름을 잡으면 여러 환경과 서비스를 구분하기 쉽다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;6. Image Tag Mutability 설정&lt;/h2&gt;

&lt;p&gt;
ECR에서는 같은 태그를 다시 push해서 덮어쓸 수 있다.
&lt;/p&gt;

&lt;p&gt;
예를 들어 다음 태그를 이미 push했다고 하자.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;app:latest
app:v1.0.0&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Tag mutability가 mutable이면 같은 태그를 다시 push할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;app:v1.0.0
→ 다시 push
→ 기존 태그가 새 이미지로 변경될 수 있음&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 방식은 편하지만 운영에서는 위험할 수 있다.
&lt;/p&gt;

&lt;p&gt;
같은 태그가 가리키는 이미지가 바뀌면, 어떤 이미지가 실제로 배포되었는지 추적하기 어려워질 수 있기 때문이다.
&lt;/p&gt;

&lt;p&gt;
그래서 운영 Repository에서는 &lt;code&gt;IMMUTABLE&lt;/code&gt;을 고려하는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecr_repository&quot; &quot;app&quot; {
  name = &quot;${var.project_name}/${var.environment}/app&quot;

  image_tag_mutability = &quot;IMMUTABLE&quot;

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-app-ecr&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이렇게 설정하면 이미 존재하는 태그로 다시 push할 때 실패한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;이미 존재하는 tag
→ 다시 push 시도
→ 실패&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
개발 환경에서는 편의상 &lt;code&gt;MUTABLE&lt;/code&gt;을 사용할 수도 있다.
&lt;/p&gt;

&lt;p&gt;
하지만 운영 환경에서는 같은 태그가 다른 이미지를 가리키지 않도록 &lt;code&gt;IMMUTABLE&lt;/code&gt;을 사용하는 편이 더 안전하다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;설정&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;의미&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;추천 상황&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;MUTABLE&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;같은 태그 덮어쓰기 가능&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;개발, 테스트&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;IMMUTABLE&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;같은 태그 덮어쓰기 방지&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;운영, 배포 추적 필요&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;hr /&gt;

&lt;h2&gt;7. Image Scanning 설정&lt;/h2&gt;

&lt;p&gt;
ECR은 이미지 취약점 스캔 기능을 제공한다.
&lt;/p&gt;

&lt;p&gt;
Repository 단위로 push 시점에 스캔하도록 설정할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecr_repository&quot; &quot;app&quot; {
  name = &quot;${var.project_name}/${var.environment}/app&quot;

  image_tag_mutability = &quot;IMMUTABLE&quot;

  image_scanning_configuration {
    scan_on_push = true
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-app-ecr&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
&lt;code&gt;scan_on_push = true&lt;/code&gt;를 설정하면 이미지가 push될 때 취약점 스캔이 수행된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;docker push
→ ECR image scan
→ scan result 확인&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 image scanning을 켰다고 해서 모든 보안 문제가 자동으로 해결되는 것은 아니다.
&lt;/p&gt;

&lt;p&gt;
스캔 결과를 확인하고, 취약한 base image나 package를 업데이트하는 운영 절차가 함께 필요하다.
&lt;/p&gt;

&lt;blockquote&gt;
  Image scanning은 보안 점검의 시작점이지, 보안 조치의 끝은 아니다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;8. Encryption 설정&lt;/h2&gt;

&lt;p&gt;
ECR에 저장되는 이미지는 암호화되어 저장된다.
&lt;/p&gt;

&lt;p&gt;
Terraform에서는 암호화 방식을 명시할 수 있다.
&lt;/p&gt;

&lt;p&gt;
기본적인 AES256 암호화는 다음처럼 작성할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecr_repository&quot; &quot;app&quot; {
  name = &quot;${var.project_name}/${var.environment}/app&quot;

  encryption_configuration {
    encryption_type = &quot;AES256&quot;
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-app-ecr&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
KMS Key를 사용하고 싶다면 다음처럼 설정할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_kms_key&quot; &quot;ecr&quot; {
  description             = &quot;KMS key for ECR&quot;
  deletion_window_in_days = 7

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-ecr-kms-key&quot;
  })
}

resource &quot;aws_ecr_repository&quot; &quot;app&quot; {
  name = &quot;${var.project_name}/${var.environment}/app&quot;

  encryption_configuration {
    encryption_type = &quot;KMS&quot;
    kms_key         = aws_kms_key.ecr.arn
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-app-ecr&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
초보 단계에서는 먼저 &lt;code&gt;AES256&lt;/code&gt;로 명시하고,
키 정책이나 감사 요구가 있는 경우 KMS를 고려해도 충분하다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;9. Lifecycle Policy 설정&lt;/h2&gt;

&lt;p&gt;
ECR은 이미지를 계속 push하면 저장된 이미지가 쌓인다.
&lt;/p&gt;

&lt;p&gt;
이미지가 계속 쌓이면 저장 비용이 증가하고, 오래된 이미지 관리도 어려워진다.
&lt;/p&gt;

&lt;p&gt;
그래서 Lifecycle Policy를 설정하는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECR Repository
→ 오래된 이미지 자동 정리&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
예를 들어 다음 정책은 untagged 이미지를 7일 후 만료시키고,
전체 이미지는 최근 20개만 남기는 예시다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecr_lifecycle_policy&quot; &quot;app&quot; {
  repository = aws_ecr_repository.app.name

  policy = jsonencode({
    rules = [
      {
        rulePriority = 1
        description  = &quot;Expire untagged images older than 7 days&quot;

        selection = {
          tagStatus   = &quot;untagged&quot;
          countType   = &quot;sinceImagePushed&quot;
          countUnit   = &quot;days&quot;
          countNumber = 7
        }

        action = {
          type = &quot;expire&quot;
        }
      },
      {
        rulePriority = 2
        description  = &quot;Keep only last 20 images&quot;

        selection = {
          tagStatus   = &quot;any&quot;
          countType   = &quot;imageCountMoreThan&quot;
          countNumber = 20
        }

        action = {
          type = &quot;expire&quot;
        }
      }
    ]
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
여기서 중요한 점은 &lt;code&gt;aws_ecr_lifecycle_policy&lt;/code&gt;는 Repository 하나에 하나만 사용하는 것이 좋다는 점이다.
&lt;/p&gt;

&lt;p&gt;
여러 정리 규칙이 필요하다면 여러 개의 lifecycle policy 리소스를 만드는 것이 아니라,
하나의 policy 안에 여러 rule을 작성한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;권장:
aws_ecr_lifecycle_policy 1개
→ rules 여러 개

비권장:
같은 repository에 aws_ecr_lifecycle_policy 여러 개&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;10. Repository Policy와 IAM 권한&lt;/h2&gt;

&lt;p&gt;
ECR 접근 권한은 크게 두 방향에서 생각할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;1. IAM Policy
   → 누가 ECR에 push / pull 할 수 있는가

2. Repository Policy
   → Repository 자체에서 어떤 주체를 허용할 것인가&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
일반적으로 같은 AWS 계정 안에서 ECS나 CI/CD가 ECR을 사용할 때는 IAM Role에 권한을 주는 방식이 많이 사용된다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;10.1 ECR Push 권한 예시&lt;/h3&gt;

&lt;p&gt;
CI/CD가 Docker 이미지를 ECR에 push하려면 다음과 같은 권한이 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_policy&quot; &quot;ecr_push&quot; {
  name = &quot;${var.project_name}-${var.environment}-ecr-push&quot;

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;ecr:GetAuthorizationToken&quot;
        ]
        Resource = &quot;*&quot;
      },
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;ecr:BatchCheckLayerAvailability&quot;,
          &quot;ecr:CompleteLayerUpload&quot;,
          &quot;ecr:InitiateLayerUpload&quot;,
          &quot;ecr:PutImage&quot;,
          &quot;ecr:UploadLayerPart&quot;
        ]
        Resource = aws_ecr_repository.app.arn
      }
    ]
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
&lt;code&gt;ecr:GetAuthorizationToken&lt;/code&gt;은 ECR 로그인에 필요하고,
나머지 권한은 이미지 layer 업로드와 image push에 필요하다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;10.2 ECR Pull 권한 예시&lt;/h3&gt;

&lt;p&gt;
ECS Task Execution Role이나 EC2가 이미지를 pull하려면 다음 권한이 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_policy&quot; &quot;ecr_pull&quot; {
  name = &quot;${var.project_name}-${var.environment}-ecr-pull&quot;

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;ecr:GetAuthorizationToken&quot;
        ]
        Resource = &quot;*&quot;
      },
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;ecr:BatchCheckLayerAvailability&quot;,
          &quot;ecr:BatchGetImage&quot;,
          &quot;ecr:GetDownloadUrlForLayer&quot;
        ]
        Resource = aws_ecr_repository.app.arn
      }
    ]
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ECS Fargate에서는 보통 Task Execution Role이 ECR pull 권한을 가진다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECS Task Execution Role
→ ECR image pull
→ Container 실행&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;10.3 Repository Policy는 언제 사용할까?&lt;/h3&gt;

&lt;p&gt;
Repository Policy는 ECR Repository 자체에 붙는 정책이다.
&lt;/p&gt;

&lt;p&gt;
예를 들어 다른 AWS 계정에서 이 Repository의 이미지를 pull해야 한다면 Repository Policy를 사용할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecr_repository_policy&quot; &quot;allow_cross_account_pull&quot; {
  repository = aws_ecr_repository.app.name

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Sid    = &quot;AllowCrossAccountPull&quot;
        Effect = &quot;Allow&quot;

        Principal = {
          AWS = &quot;arn:aws:iam::123456789012:root&quot;
        }

        Action = [
          &quot;ecr:BatchGetImage&quot;,
          &quot;ecr:GetDownloadUrlForLayer&quot;
        ]
      }
    ]
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
초보 단계에서는 먼저 같은 계정 내 IAM Role 권한 부여 방식부터 익히는 것이 좋다.
&lt;/p&gt;

&lt;p&gt;
다른 계정 접근이나 조직 단위 공유가 필요할 때 Repository Policy를 고려하면 된다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;11. ECS에서 사용할 Repository URL 출력하기&lt;/h2&gt;

&lt;p&gt;
ECR Repository를 만들면 ECS Task Definition에서 사용할 이미지 URL이 필요하다.
&lt;/p&gt;

&lt;p&gt;
Terraform에서는 &lt;code&gt;repository_url&lt;/code&gt;을 output으로 만들 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;output &quot;ecr_repository_url&quot; {
  description = &quot;ECR repository URL&quot;
  value       = aws_ecr_repository.app.repository_url
}

output &quot;ecr_repository_arn&quot; {
  description = &quot;ECR repository ARN&quot;
  value       = aws_ecr_repository.app.arn
}

output &quot;ecr_repository_name&quot; {
  description = &quot;ECR repository name&quot;
  value       = aws_ecr_repository.app.name
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ECS Task Definition에서는 보통 다음과 같은 형태의 이미지 URI를 사용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;${repository_url}:${image_tag}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
예를 들어 다음과 같은 값이 된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/demo/prod/app:v1.0.0&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 값은 나중에 ECS Task Definition이나 CI/CD에서 사용된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECR repository_url
→ Docker tag
→ ECS image URI&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;12. 실전 예제: 애플리케이션용 ECR 구성&lt;/h2&gt;

&lt;p&gt;
이제 애플리케이션용 ECR Repository 구성을 하나로 정리해보자.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;12.1 variables.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;variable &quot;project_name&quot; {
  description = &quot;Project name&quot;
  type        = string
}

variable &quot;environment&quot; {
  description = &quot;Environment name&quot;
  type        = string
}

variable &quot;repository_name&quot; {
  description = &quot;ECR repository name&quot;
  type        = string
  default     = &quot;app&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;12.2 locals.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = &quot;terraform&quot;
  }

  ecr_name = &quot;${var.project_name}/${var.environment}/${var.repository_name}&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;12.3 main.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecr_repository&quot; &quot;app&quot; {
  name = local.ecr_name

  image_tag_mutability = &quot;IMMUTABLE&quot;

  image_scanning_configuration {
    scan_on_push = true
  }

  encryption_configuration {
    encryption_type = &quot;AES256&quot;
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-${var.repository_name}-ecr&quot;
  })
}

resource &quot;aws_ecr_lifecycle_policy&quot; &quot;app&quot; {
  repository = aws_ecr_repository.app.name

  policy = jsonencode({
    rules = [
      {
        rulePriority = 1
        description  = &quot;Expire untagged images older than 7 days&quot;

        selection = {
          tagStatus   = &quot;untagged&quot;
          countType   = &quot;sinceImagePushed&quot;
          countUnit   = &quot;days&quot;
          countNumber = 7
        }

        action = {
          type = &quot;expire&quot;
        }
      },
      {
        rulePriority = 2
        description  = &quot;Keep only last 20 images&quot;

        selection = {
          tagStatus   = &quot;any&quot;
          countType   = &quot;imageCountMoreThan&quot;
          countNumber = 20
        }

        action = {
          type = &quot;expire&quot;
        }
      }
    ]
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;12.4 outputs.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;output &quot;ecr_repository_url&quot; {
  description = &quot;ECR repository URL&quot;
  value       = aws_ecr_repository.app.repository_url
}

output &quot;ecr_repository_arn&quot; {
  description = &quot;ECR repository ARN&quot;
  value       = aws_ecr_repository.app.arn
}

output &quot;ecr_repository_name&quot; {
  description = &quot;ECR repository name&quot;
  value       = aws_ecr_repository.app.name
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 구성의 특징은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Repository 이름에 project / environment / service 반영
Tag immutability 활성화
Image scan on push 활성화
AES256 암호화 명시
Lifecycle Policy로 오래된 이미지 정리
ECS와 CI/CD에서 사용할 repository_url output 제공&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;13. 의존성 흐름&lt;/h2&gt;

&lt;p&gt;
ECR을 Terraform으로 구현할 때의 의존성 흐름은 비교적 단순하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECR Repository
→ Lifecycle Policy
→ IAM Policy
→ ECS / CI/CD&lt;/code&gt;&lt;/pre&gt;

&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/letlC/dJMcafGC1Wo/6DnTKk7YaIP93MJiyfSukK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/letlC/dJMcafGC1Wo/6DnTKk7YaIP93MJiyfSukK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/letlC/dJMcafGC1Wo/6DnTKk7YaIP93MJiyfSukK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FletlC%2FdJMcafGC1Wo%2F6DnTKk7YaIP93MJiyfSukK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;p&gt;
이 구조에서 중요한 점은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lifecycle Policy는 ECR Repository 이름을 참조한다.
Push IAM Policy는 ECR Repository ARN을 참조한다.
Pull IAM Policy도 ECR Repository ARN을 참조한다.
ECS나 CI/CD는 IAM Role을 통해 ECR에 접근한다.&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
  ECR Repository는 이미지를 저장하는 리소스이고, IAM Policy는 누가 push 또는 pull 할 수 있는지를 제어한다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;14. 자주 하는 실수&lt;/h2&gt;

&lt;h3&gt;14.1 Terraform으로 docker push까지 하려고 함&lt;/h3&gt;

&lt;p&gt;
Terraform은 인프라 리소스를 선언하고 관리하는 도구다.
&lt;/p&gt;

&lt;p&gt;
Docker 이미지를 빌드하고 push하는 작업은 보통 CI/CD에서 처리하는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Terraform
→ ECR Repository 생성

CI/CD
→ docker build
→ docker push
→ ECS 배포&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.2 latest 태그만 사용함&lt;/h3&gt;

&lt;p&gt;
&lt;code&gt;latest&lt;/code&gt; 태그만 사용하면 어떤 이미지가 실제로 배포되었는지 추적하기 어렵다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;비추천:
app:latest

추천:
app:v1.0.0
app:git-sha-abc1234
app:20260514-1530&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
운영에서는 Git SHA, 버전 번호, 빌드 번호 같은 고유한 태그를 사용하는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.3 운영 Repository를 MUTABLE로 둠&lt;/h3&gt;

&lt;p&gt;
Tag mutability가 mutable이면 같은 태그를 다시 push해서 덮어쓸 수 있다.
&lt;/p&gt;

&lt;p&gt;
운영에서는 배포 추적을 위해 &lt;code&gt;IMMUTABLE&lt;/code&gt;을 고려하는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;운영 권장:
image_tag_mutability = &quot;IMMUTABLE&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.4 Lifecycle Policy를 설정하지 않음&lt;/h3&gt;

&lt;p&gt;
이미지를 계속 push하면 ECR에 이미지가 계속 쌓인다.
&lt;/p&gt;

&lt;p&gt;
오래된 이미지를 정리하지 않으면 저장 비용이 증가하고 관리도 어려워진다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECR images 계속 증가
→ 저장 비용 증가
→ Lifecycle Policy 필요&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.5 untagged 이미지를 방치함&lt;/h3&gt;

&lt;p&gt;
배포 과정에서 태그가 바뀌거나 이미지를 덮어쓰다 보면 untagged 이미지가 남을 수 있다.
&lt;/p&gt;

&lt;p&gt;
이런 이미지는 사용 중인지 확인하기 어렵고 비용만 발생시킬 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;untagged images
→ 7일 후 삭제&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Lifecycle Policy로 정리하는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.6 ECS Task Execution Role에 ECR Pull 권한이 없음&lt;/h3&gt;

&lt;p&gt;
ECS가 ECR 이미지를 가져오려면 Task Execution Role에 ECR pull 권한이 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ECS Task Execution Role
→ ecr:GetAuthorizationToken
→ ecr:BatchGetImage
→ ecr:GetDownloadUrlForLayer&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
권한이 부족하면 ECS Task가 이미지를 pull하지 못하고 실행에 실패할 수 있다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.7 Repository URL을 output으로 만들지 않음&lt;/h3&gt;

&lt;p&gt;
ECR Repository URL은 ECS Task Definition, CI/CD, Docker tag 명령에서 자주 사용된다.
&lt;/p&gt;

&lt;p&gt;
따라서 output으로 만들어두면 이후 글이나 모듈에서 참조하기 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;output &quot;ecr_repository_url&quot; {
  value = aws_ecr_repository.app.repository_url
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.8 ECR 삭제 시 이미지가 남아 있어 실패함&lt;/h3&gt;

&lt;p&gt;
ECR Repository 안에 이미지가 남아 있으면 삭제가 실패할 수 있다.
&lt;/p&gt;

&lt;p&gt;
학습용에서는 삭제 전에 이미지를 비우거나, Terraform 리소스에서 &lt;code&gt;force_delete&lt;/code&gt; 사용을 고려할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecr_repository&quot; &quot;app&quot; {
  name         = local.ecr_name
  force_delete = true
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 운영 Repository에서 &lt;code&gt;force_delete = true&lt;/code&gt;를 사용하는 것은 위험할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;학습용:
force_delete = true 고려 가능

운영:
신중하게 사용&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;15. 마무리&lt;/h2&gt;

&lt;p&gt;
이번 글에서는 Terraform으로 ECR을 구현하는 방법을 정리했다.
&lt;/p&gt;

&lt;p&gt;
ECR은 단순한 Docker 이미지 저장소처럼 보이지만, 실제 운영에서는 다음 설정을 함께 고려해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Repository 이름 규칙
Tag immutability
Image scanning
Encryption
Lifecycle Policy
IAM Push / Pull 권한
repository_url output&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
특히 Terraform과 CI/CD의 역할을 분리해서 생각하는 것이 중요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Terraform
→ ECR Repository와 정책 구성

CI/CD
→ Docker image build / push / deploy&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ECR은 이후 ECS Fargate 글에서 다시 등장한다.
&lt;/p&gt;

&lt;p&gt;
ECS Task Definition은 ECR의 &lt;code&gt;repository_url&lt;/code&gt;과 image tag를 조합해서 컨테이너 이미지를 실행하게 된다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 style=&quot;text-align:center;&quot;&gt;한 줄 정리&lt;/h3&gt;

&lt;p style=&quot;text-align:center;&quot;&gt;
&lt;strong&gt;ECR은 Docker 이미지를 저장하는 기반 리소스이고, Terraform은 저장소와 정책을 만들고 CI/CD는 이미지를 push한다.&lt;/strong&gt;
&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;
다음 글에서는 ECS Fargate를 Terraform으로 구현해본다.
ECS는 ECR에 저장된 이미지를 가져와 컨테이너를 실행하고, ALB Target Group과 연결해 실제 트래픽을 처리하는 리소스다.
&lt;/p&gt;</description>
      <category>테라폼</category>
      <author>pininini</author>
      <guid isPermaLink="true">https://pininininfra.tistory.com/22</guid>
      <comments>https://pininininfra.tistory.com/22#entry22comment</comments>
      <pubDate>Thu, 14 May 2026 19:31:21 +0900</pubDate>
    </item>
    <item>
      <title>4-9. 테라폼 - ALB와 Target Group 구현하기</title>
      <link>https://pininininfra.tistory.com/21</link>
      <description>&lt;h1 style=&quot;text-align:center;&quot;&gt;테라폼 - ALB와 Target Group 구현하기&lt;/h1&gt;
&lt;p style=&quot;text-align:center;&quot;&gt;&lt;em&gt;외부 요청을 애플리케이션 서버로 안전하게 전달하기&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;
이전 글에서는 Terraform으로 Secrets Manager와 SSM Parameter Store를 구현하는 방법을 정리했다.
&lt;/p&gt;

&lt;p&gt;
이번 글에서는 AWS에서 웹 서비스를 구성할 때 자주 사용하는 &lt;strong&gt;ALB&lt;/strong&gt;와 &lt;strong&gt;Target Group&lt;/strong&gt;을 Terraform으로 구현해보려 한다.
&lt;/p&gt;

&lt;p&gt;
ALB는 Application Load Balancer의 약자다.
&lt;/p&gt;

&lt;p&gt;
사용자의 HTTP 또는 HTTPS 요청을 받아 뒤쪽의 EC2, ECS Task, Lambda, IP Target 등으로 전달하는 역할을 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;User
→ ALB
→ Target Group
→ EC2 / ECS / Lambda&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
처음에는 ALB를 단순히 “트래픽을 나눠주는 리소스” 정도로 이해할 수 있다.
&lt;/p&gt;

&lt;p&gt;
하지만 실제로는 Listener, Target Group, Health Check, Security Group, 인증서, Redirect 설정까지 함께 이해해야 제대로 사용할 수 있다.
&lt;/p&gt;

&lt;blockquote&gt;
  ALB는 외부 요청을 받는 입구이고, Target Group은 요청을 전달할 대상 목록이다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;목차&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;1. ALB란 무엇인가&lt;/li&gt;
  &lt;li&gt;2. ALB를 구성하는 주요 요소&lt;/li&gt;
  &lt;li&gt;3. ALB와 Target Group의 관계&lt;/li&gt;
  &lt;li&gt;4. Public ALB와 Internal ALB&lt;/li&gt;
  &lt;li&gt;5. ALB Security Group 구성&lt;/li&gt;
  &lt;li&gt;6. Target Group 만들기&lt;/li&gt;
  &lt;li&gt;7. ALB 만들기&lt;/li&gt;
  &lt;li&gt;8. Listener 만들기&lt;/li&gt;
  &lt;li&gt;9. Target Group Attachment&lt;/li&gt;
  &lt;li&gt;10. HTTPS Listener와 HTTP Redirect&lt;/li&gt;
  &lt;li&gt;11. Listener Rule로 경로 기반 라우팅하기&lt;/li&gt;
  &lt;li&gt;12. 실전 예제: ALB → EC2 구조&lt;/li&gt;
  &lt;li&gt;13. 의존성 흐름&lt;/li&gt;
  &lt;li&gt;14. 자주 하는 실수&lt;/li&gt;
  &lt;li&gt;15. 마무리&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2&gt;1. ALB란 무엇인가&lt;/h2&gt;

&lt;p&gt;
ALB는 Application Load Balancer의 약자다.
&lt;/p&gt;

&lt;p&gt;
AWS Elastic Load Balancing 서비스의 한 종류이며, HTTP와 HTTPS 요청을 애플리케이션 계층에서 처리한다.
&lt;/p&gt;

&lt;p&gt;
ALB는 다음과 같은 상황에서 자주 사용된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;여러 EC2로 트래픽 분산
ECS Fargate 서비스 앞단 구성
HTTPS 인증서 연결
HTTP 요청을 HTTPS로 Redirect
/api, /admin 같은 경로 기반 라우팅
정상 서버에만 트래픽 전달&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
예를 들어 사용자가 웹 서비스에 접속하면 ALB가 요청을 받고, 뒤쪽 Target Group에 등록된 정상 대상에게 트래픽을 전달한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;User
→ ALB
→ Target Group
→ Healthy Target&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
여기서 중요한 것은 ALB가 단순히 요청을 전달하는 것만 하는 게 아니라는 점이다.
&lt;/p&gt;

&lt;p&gt;
ALB는 Health Check를 통해 정상 대상만 골라서 트래픽을 보낼 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;정상 Target
→ 트래픽 전달

비정상 Target
→ 트래픽 제외&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;2. ALB를 구성하는 주요 요소&lt;/h2&gt;

&lt;p&gt;
ALB를 Terraform으로 구현할 때는 다음 리소스를 함께 봐야 한다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;구성 요소&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;Terraform 리소스&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;역할&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ALB&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_lb&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Load Balancer 본체&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Target Group&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_lb_target_group&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;트래픽을 전달할 대상 그룹&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Listener&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_lb_listener&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ALB가 받을 포트와 프로토콜&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Listener Rule&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_lb_listener_rule&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;경로, Host 조건에 따른 라우팅&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Target Attachment&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_lb_target_group_attachment&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;EC2 같은 대상을 Target Group에 등록&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Security Group&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;aws_security_group&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ALB와 App 간 네트워크 접근 제어&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
구조를 단순화하면 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ALB
→ Listener
→ Target Group
→ Target&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
사용자는 ALB로 접속하고, ALB는 Listener 규칙을 보고 Target Group으로 요청을 보낸다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;3. ALB와 Target Group의 관계&lt;/h2&gt;

&lt;p&gt;
ALB와 Target Group은 서로 역할이 다르다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ALB
→ 요청을 받는 입구

Listener
→ 어떤 포트와 프로토콜로 받을지 결정

Target Group
→ 요청을 보낼 대상 목록

Target
→ 실제 요청을 처리하는 EC2, ECS Task, Lambda 등&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
예를 들어 다음 구조를 생각해보자.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;User
→ ALB: 80
→ Listener: HTTP 80
→ Target Group: app-tg
→ EC2: 8080&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
사용자는 ALB의 80번 포트로 요청한다.
&lt;/p&gt;

&lt;p&gt;
하지만 실제 애플리케이션 서버는 8080번 포트에서 동작할 수 있다.
&lt;/p&gt;

&lt;p&gt;
이 경우 ALB Listener는 80번 포트에서 요청을 받고, Target Group은 EC2의 8080번 포트로 트래픽을 전달한다.
&lt;/p&gt;

&lt;blockquote&gt;
  ALB가 받는 포트와 애플리케이션이 실제로 듣는 포트는 다를 수 있다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;4. Public ALB와 Internal ALB&lt;/h2&gt;

&lt;p&gt;
ALB는 크게 두 가지 방식으로 만들 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Internet-facing ALB
Internal ALB&lt;/code&gt;&lt;/pre&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;구분&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;의미&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;사용 예&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Internet-facing&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;인터넷에서 접근 가능한 ALB&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;외부 웹 서비스, API 서버&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Internal&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;VPC 내부에서만 접근 가능한 ALB&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;내부 API, 마이크로서비스 통신&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
외부 사용자가 접속하는 웹 서비스라면 보통 Internet-facing ALB를 사용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Internet
→ Public ALB
→ Private EC2 / ECS&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
반대로 내부 서비스 간 통신에는 Internal ALB를 사용할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Service A
→ Internal ALB
→ Service B&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 글에서는 초보자가 이해하기 쉬운 Internet-facing ALB 기준으로 설명한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;5. ALB Security Group 구성&lt;/h2&gt;

&lt;p&gt;
ALB를 사용할 때 Security Group은 보통 두 개로 나누어 생각한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ALB Security Group
→ 외부 사용자가 ALB에 접근할 수 있게 허용

App Security Group
→ ALB에서 오는 요청만 애플리케이션 서버에 허용&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
예를 들어 웹 서비스 구조는 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;User
→ ALB Security Group: 80 또는 443 허용
→ App Security Group: ALB Security Group에서 오는 8080만 허용&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Terraform 코드로 보면 먼저 ALB Security Group을 만든다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;alb&quot; {
  name        = &quot;${var.project_name}-alb-sg&quot;
  description = &quot;Security group for ALB&quot;
  vpc_id      = var.vpc_id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-alb-sg&quot;
  })
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;alb_http&quot; {
  security_group_id = aws_security_group.alb.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;tcp&quot;
  from_port   = 80
  to_port     = 80

  description = &quot;Allow HTTP from anywhere&quot;
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;alb_https&quot; {
  security_group_id = aws_security_group.alb.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;tcp&quot;
  from_port   = 443
  to_port     = 443

  description = &quot;Allow HTTPS from anywhere&quot;
}

resource &quot;aws_vpc_security_group_egress_rule&quot; &quot;alb_all&quot; {
  security_group_id = aws_security_group.alb.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;-1&quot;

  description = &quot;Allow all outbound traffic&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
그리고 애플리케이션 서버 Security Group에서는 ALB Security Group에서 오는 요청만 허용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;app&quot; {
  name        = &quot;${var.project_name}-app-sg&quot;
  description = &quot;Security group for application&quot;
  vpc_id      = var.vpc_id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-app-sg&quot;
  })
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;app_from_alb&quot; {
  security_group_id = aws_security_group.app.id

  referenced_security_group_id = aws_security_group.alb.id
  ip_protocol                  = &quot;tcp&quot;
  from_port                    = 8080
  to_port                      = 8080

  description = &quot;Allow app traffic from ALB&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 구조의 핵심은 EC2나 ECS를 인터넷 전체에 직접 열지 않는다는 점이다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;권장 구조:
Internet → ALB → App

피하고 싶은 구조:
Internet → App 직접 접근&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;6. Target Group 만들기&lt;/h2&gt;

&lt;p&gt;
Target Group은 ALB가 요청을 전달할 대상 그룹이다.
&lt;/p&gt;

&lt;p&gt;
Target Group에는 대상 타입이 있다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;target_type&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;의미&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;사용 예&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;instance&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;EC2 Instance ID를 대상으로 등록&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;EC2 기반 서비스&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ip&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;IP 주소를 대상으로 등록&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECS Fargate, IP Target&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;lambda&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lambda 함수를 대상으로 등록&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Lambda 기반 HTTP 처리&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
EC2를 대상으로 하는 Target Group은 다음처럼 만들 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_lb_target_group&quot; &quot;app&quot; {
  name        = &quot;${var.project_name}-app-tg&quot;
  port        = 8080
  protocol    = &quot;HTTP&quot;
  vpc_id      = var.vpc_id
  target_type = &quot;instance&quot;

  health_check {
    enabled             = true
    path                = &quot;/health&quot;
    matcher             = &quot;200&quot;
    interval            = 30
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-app-tg&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
여기서 중요한 설정은 &lt;code&gt;health_check&lt;/code&gt;다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;path = &quot;/health&quot;
matcher = &quot;200&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ALB는 이 경로로 주기적으로 요청을 보내고, 정상 응답이 오면 해당 Target을 healthy 상태로 본다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;GET /health
→ 200 응답
→ Healthy&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
반대로 애플리케이션이 &lt;code&gt;/health&lt;/code&gt; 경로를 제공하지 않거나, 200이 아닌 응답을 반환하면 Target이 unhealthy가 될 수 있다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;7. ALB 만들기&lt;/h2&gt;

&lt;p&gt;
이제 ALB 본체를 만든다.
&lt;/p&gt;

&lt;p&gt;
Internet-facing ALB는 보통 Public Subnet에 배치한다.
&lt;/p&gt;

&lt;p&gt;
ALB는 최소 두 개 이상의 서로 다른 AZ Subnet을 사용하는 것이 일반적이며, AWS에서도 Application Load Balancer에는 최소 두 개 이상의 AZ Subnet을 요구한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Public Subnet A
Public Subnet C
→ ALB&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Terraform 코드는 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_lb&quot; &quot;app&quot; {
  name               = &quot;${var.project_name}-alb&quot;
  load_balancer_type = &quot;application&quot;
  internal           = false

  security_groups = [
    aws_security_group.alb.id
  ]

  subnets = var.public_subnet_ids

  enable_deletion_protection = false

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-alb&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
주요 설정은 다음과 같다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;설정&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;의미&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;load_balancer_type&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;application이면 ALB 생성&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;internal&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;false면 Internet-facing, true면 Internal&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;security_groups&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ALB에 연결할 Security Group&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;subnets&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ALB가 배치될 Subnet 목록&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;enable_deletion_protection&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ALB 삭제 보호 여부&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
학습용에서는 &lt;code&gt;enable_deletion_protection = false&lt;/code&gt;로 둘 수 있다.
&lt;/p&gt;

&lt;p&gt;
운영 환경에서는 실수로 ALB가 삭제되지 않도록 삭제 보호를 켜는 것도 고려할 수 있다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;8. Listener 만들기&lt;/h2&gt;

&lt;p&gt;
Listener는 ALB가 어떤 포트와 프로토콜로 요청을 받을지 정의한다.
&lt;/p&gt;

&lt;p&gt;
가장 기본적인 HTTP Listener는 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_lb_listener&quot; &quot;http&quot; {
  load_balancer_arn = aws_lb.app.arn
  port              = 80
  protocol          = &quot;HTTP&quot;

  default_action {
    type             = &quot;forward&quot;
    target_group_arn = aws_lb_target_group.app.arn
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 코드는 다음 의미다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ALB의 80번 포트로 들어온 HTTP 요청을
app Target Group으로 전달한다.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
여기서 Listener는 ALB와 Target Group을 모두 참조한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;aws_lb.app
→ aws_lb_listener.http

aws_lb_target_group.app
→ aws_lb_listener.http&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;9. Target Group Attachment&lt;/h2&gt;

&lt;p&gt;
Target Group을 만들었다고 해서 EC2가 자동으로 등록되는 것은 아니다.
&lt;/p&gt;

&lt;p&gt;
EC2를 Target Group에 등록해야 한다.
&lt;/p&gt;

&lt;p&gt;
EC2 기반 구조에서는 &lt;code&gt;aws_lb_target_group_attachment&lt;/code&gt;를 사용할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_lb_target_group_attachment&quot; &quot;app&quot; {
  target_group_arn = aws_lb_target_group.app.arn
  target_id        = aws_instance.app.id
  port             = 8080
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
여기서 중요한 설정은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;target_group_arn
→ 등록할 Target Group

target_id
→ 등록할 EC2 Instance ID

port
→ Target이 실제로 요청을 받을 포트&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
만약 애플리케이션이 EC2 내부에서 8080번 포트로 실행 중이라면 Target Group Attachment의 port도 8080으로 맞춰야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ALB Listener: 80
Target Group: 8080
EC2 App: 8080&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ECS Fargate에서는 보통 Target Group Attachment를 직접 작성하지 않는다.
&lt;/p&gt;

&lt;p&gt;
ECS Service의 &lt;code&gt;load_balancer&lt;/code&gt; 블록에서 Target Group을 연결하면 ECS가 Task를 Target Group에 등록한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;10. HTTPS Listener와 HTTP Redirect&lt;/h2&gt;

&lt;p&gt;
운영 환경에서는 HTTP보다 HTTPS를 사용하는 것이 일반적이다.
&lt;/p&gt;

&lt;p&gt;
ALB에서 HTTPS를 사용하려면 ACM 인증서가 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ACM Certificate
→ ALB HTTPS Listener
→ Target Group&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
HTTPS Listener는 다음처럼 만들 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_lb_listener&quot; &quot;https&quot; {
  load_balancer_arn = aws_lb.app.arn
  port              = 443
  protocol          = &quot;HTTPS&quot;
  certificate_arn   = var.acm_certificate_arn

  ssl_policy = &quot;ELBSecurityPolicy-TLS13-1-2-2021-06&quot;

  default_action {
    type             = &quot;forward&quot;
    target_group_arn = aws_lb_target_group.app.arn
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
그리고 HTTP 요청은 HTTPS로 Redirect할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_lb_listener&quot; &quot;http&quot; {
  load_balancer_arn = aws_lb.app.arn
  port              = 80
  protocol          = &quot;HTTP&quot;

  default_action {
    type = &quot;redirect&quot;

    redirect {
      port        = &quot;443&quot;
      protocol    = &quot;HTTPS&quot;
      status_code = &quot;HTTP_301&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이렇게 하면 사용자가 HTTP로 접속해도 HTTPS로 이동한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;http://example.com
→ https://example.com&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
학습용 예제에서는 HTTP Listener만으로 시작해도 된다.
&lt;/p&gt;

&lt;p&gt;
하지만 실제 서비스에서는 ACM 인증서와 HTTPS Listener를 함께 구성하는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;11. Listener Rule로 경로 기반 라우팅하기&lt;/h2&gt;

&lt;p&gt;
ALB는 경로 기반 라우팅을 지원한다.
&lt;/p&gt;

&lt;p&gt;
예를 들어 요청 경로에 따라 서로 다른 Target Group으로 보낼 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/api/*
→ api Target Group

/admin/*
→ admin Target Group&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Terraform 예시는 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_lb_target_group&quot; &quot;admin&quot; {
  name        = &quot;${var.project_name}-admin-tg&quot;
  port        = 8081
  protocol    = &quot;HTTP&quot;
  vpc_id      = var.vpc_id
  target_type = &quot;instance&quot;

  health_check {
    path    = &quot;/health&quot;
    matcher = &quot;200&quot;
  }
}

resource &quot;aws_lb_listener_rule&quot; &quot;admin&quot; {
  listener_arn = aws_lb_listener.https.arn
  priority     = 100

  condition {
    path_pattern {
      values = [&quot;/admin/*&quot;]
    }
  }

  action {
    type             = &quot;forward&quot;
    target_group_arn = aws_lb_target_group.admin.arn
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 구조를 사용하면 하나의 ALB로 여러 애플리케이션을 라우팅할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;example.com/api/*
→ API 서버

example.com/admin/*
→ 관리자 서버&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 초보 단계에서는 먼저 기본 Listener와 하나의 Target Group 구조를 이해한 뒤, 이후 Listener Rule을 확장하는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;12. 실전 예제: ALB → EC2 구조&lt;/h2&gt;

&lt;p&gt;
이제 ALB에서 EC2로 트래픽을 전달하는 기본 구조를 하나로 정리해보자.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Internet
→ ALB
→ Target Group
→ EC2&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;12.1 variables.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;variable &quot;project_name&quot; {
  description = &quot;Project name&quot;
  type        = string
}

variable &quot;vpc_id&quot; {
  description = &quot;VPC ID&quot;
  type        = string
}

variable &quot;public_subnet_ids&quot; {
  description = &quot;Public subnet IDs for ALB&quot;
  type        = list(string)
}

variable &quot;acm_certificate_arn&quot; {
  description = &quot;ACM certificate ARN for HTTPS listener&quot;
  type        = string
  default     = null
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;12.2 locals.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;locals {
  common_tags = {
    Project   = var.project_name
    ManagedBy = &quot;terraform&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;12.3 main.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;alb&quot; {
  name        = &quot;${var.project_name}-alb-sg&quot;
  description = &quot;Security group for ALB&quot;
  vpc_id      = var.vpc_id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-alb-sg&quot;
  })
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;alb_http&quot; {
  security_group_id = aws_security_group.alb.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;tcp&quot;
  from_port   = 80
  to_port     = 80
}

resource &quot;aws_vpc_security_group_egress_rule&quot; &quot;alb_all&quot; {
  security_group_id = aws_security_group.alb.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;-1&quot;
}

resource &quot;aws_lb&quot; &quot;app&quot; {
  name               = &quot;${var.project_name}-alb&quot;
  load_balancer_type = &quot;application&quot;
  internal           = false

  security_groups = [
    aws_security_group.alb.id
  ]

  subnets = var.public_subnet_ids

  enable_deletion_protection = false

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-alb&quot;
  })
}

resource &quot;aws_lb_target_group&quot; &quot;app&quot; {
  name        = &quot;${var.project_name}-app-tg&quot;
  port        = 8080
  protocol    = &quot;HTTP&quot;
  vpc_id      = var.vpc_id
  target_type = &quot;instance&quot;

  health_check {
    enabled             = true
    path                = &quot;/health&quot;
    matcher             = &quot;200&quot;
    interval            = 30
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-app-tg&quot;
  })
}

resource &quot;aws_lb_listener&quot; &quot;http&quot; {
  load_balancer_arn = aws_lb.app.arn
  port              = 80
  protocol          = &quot;HTTP&quot;

  default_action {
    type             = &quot;forward&quot;
    target_group_arn = aws_lb_target_group.app.arn
  }
}

resource &quot;aws_lb_target_group_attachment&quot; &quot;app&quot; {
  target_group_arn = aws_lb_target_group.app.arn
  target_id        = aws_instance.app.id
  port             = 8080
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 예제는 HTTP 기반의 가장 단순한 ALB → EC2 구조다.
&lt;/p&gt;

&lt;p&gt;
실제 운영에서는 HTTPS Listener, ACM 인증서, HTTP to HTTPS Redirect를 추가하는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;12.4 outputs.tf&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;output &quot;alb_dns_name&quot; {
  description = &quot;ALB DNS name&quot;
  value       = aws_lb.app.dns_name
}

output &quot;alb_arn&quot; {
  description = &quot;ALB ARN&quot;
  value       = aws_lb.app.arn
}

output &quot;target_group_arn&quot; {
  description = &quot;Target Group ARN&quot;
  value       = aws_lb_target_group.app.arn
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ALB 생성 후 브라우저에서 다음 주소로 접속할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;http://ALB_DNS_NAME&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 Target Group Health Check가 통과해야 정상적으로 응답을 받을 수 있다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;13. 의존성 흐름&lt;/h2&gt;

&lt;p&gt;
ALB와 Target Group을 Terraform으로 구현할 때의 의존성 흐름은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Subnet / Security Group
→ ALB
→ Listener
→ Target Group
→ Target Attachment&lt;/code&gt;&lt;/pre&gt;

&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3gcOy/dJMcai4kUAm/8KsGjbN9E9OhaATVFkRMX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3gcOy/dJMcai4kUAm/8KsGjbN9E9OhaATVFkRMX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3gcOy/dJMcai4kUAm/8KsGjbN9E9OhaATVFkRMX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3gcOy%2FdJMcai4kUAm%2F8KsGjbN9E9OhaATVFkRMX1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;p&gt;
이 구조에서 중요한 점은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ALB는 Subnet과 Security Group을 참조한다.
Listener는 ALB와 Target Group을 참조한다.
Target Attachment는 Target Group과 EC2를 참조한다.&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
  ALB는 요청을 받는 리소스이고, Target Group은 요청을 전달할 대상을 관리하는 리소스다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;14. 자주 하는 실수&lt;/h2&gt;

&lt;h3&gt;14.1 ALB를 하나의 Subnet에만 배치하려고 함&lt;/h3&gt;

&lt;p&gt;
Application Load Balancer는 최소 두 개 이상의 서로 다른 AZ Subnet이 필요하다.
&lt;/p&gt;

&lt;p&gt;
따라서 Public Subnet을 하나만 만든 상태에서 ALB를 생성하려고 하면 오류가 발생할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;권장:
public-subnet-a
public-subnet-c

ALB subnets = [subnet-a, subnet-c]&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.2 ALB Security Group만 열고 App Security Group을 열지 않음&lt;/h3&gt;

&lt;p&gt;
사용자가 ALB에 접근할 수 있어도 ALB가 애플리케이션 서버에 접근하지 못하면 서비스가 동작하지 않는다.
&lt;/p&gt;

&lt;p&gt;
App Security Group에서 ALB Security Group을 source로 허용해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ALB Security Group
→ App Security Group 8080 허용&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.3 Health Check 경로가 실제 애플리케이션에 없음&lt;/h3&gt;

&lt;p&gt;
Target Group Health Check 경로가 &lt;code&gt;/health&lt;/code&gt;인데 애플리케이션이 해당 경로를 제공하지 않으면 Target이 unhealthy가 된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Target Group Health Check
→ /health

Application
→ /health 없음

결과
→ Unhealthy&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Spring Boot를 사용한다면 Actuator의 &lt;code&gt;/actuator/health&lt;/code&gt;를 사용할 수도 있다.
&lt;/p&gt;

&lt;p&gt;
이 경우 Target Group의 health_check path도 그에 맞게 바꿔야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;path = &quot;/actuator/health&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.4 Target Group port와 애플리케이션 port가 다름&lt;/h3&gt;

&lt;p&gt;
애플리케이션이 8080에서 실행 중인데 Target Group을 80으로 설정하면 Health Check나 요청 전달이 실패할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Application port: 8080
Target Group port: 80

결과:
연결 실패 가능&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
애플리케이션이 실제로 listen 중인 포트와 Target Group 설정을 맞춰야 한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.5 HTTPS Listener에 ACM 인증서를 연결하지 않음&lt;/h3&gt;

&lt;p&gt;
HTTPS Listener를 만들려면 인증서가 필요하다.
&lt;/p&gt;

&lt;p&gt;
ALB에서는 보통 ACM 인증서를 사용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTTPS Listener
→ certificate_arn 필요&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
도메인과 인증서 구성은 이후 CloudFront, ACM, Route53 글에서 더 자세히 다루는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.6 ECS Fargate에서 target_type을 instance로 둠&lt;/h3&gt;

&lt;p&gt;
ECS Fargate는 일반적으로 &lt;code&gt;target_type = &quot;ip&quot;&lt;/code&gt;를 사용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EC2 기반
→ target_type = &quot;instance&quot;

ECS Fargate
→ target_type = &quot;ip&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ECS 글에서 Fargate와 Target Group 연결을 다시 다룰 예정이다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.7 ALB 비용을 잊음&lt;/h3&gt;

&lt;p&gt;
ALB는 생성해두면 비용이 발생한다.
&lt;/p&gt;

&lt;p&gt;
학습용으로 만들었다면 실습 후 삭제하는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;terraform destroy&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
단, ALB 삭제 보호를 켜두었다면 삭제 전에 해당 설정을 해제해야 한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;15. 마무리&lt;/h2&gt;

&lt;p&gt;
이번 글에서는 Terraform으로 ALB와 Target Group을 구현하는 방법을 정리했다.
&lt;/p&gt;

&lt;p&gt;
ALB를 이해할 때 중요한 요소는 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ALB
→ 외부 요청을 받는 입구

Listener
→ 어떤 포트와 프로토콜로 요청을 받을지 정의

Target Group
→ 요청을 전달할 대상 목록

Health Check
→ 정상 대상인지 확인

Target Attachment
→ EC2 같은 대상을 Target Group에 등록

Security Group
→ ALB와 App 사이의 접근 제어&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
ALB는 단순히 트래픽을 나눠주는 리소스가 아니다.
&lt;/p&gt;

&lt;p&gt;
Health Check를 통해 정상 대상에게만 트래픽을 보내고, Listener Rule을 통해 요청 경로별로 다른 Target Group에 라우팅할 수 있다.
&lt;/p&gt;

&lt;p&gt;
또한 운영 환경에서는 HTTPS Listener와 HTTP to HTTPS Redirect, ACM 인증서 구성을 함께 고려해야 한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 style=&quot;text-align:center;&quot;&gt;한 줄 정리&lt;/h3&gt;

&lt;p style=&quot;text-align:center;&quot;&gt;
&lt;strong&gt;ALB는 요청을 받는 입구이고, Target Group은 요청을 보낼 대상 목록이며, Health Check는 정상 대상만 선택하기 위한 기준이다.&lt;/strong&gt;
&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;
다음 글에서는 ECR을 Terraform으로 구현해본다.
ECR은 Docker 이미지를 저장하는 AWS 컨테이너 이미지 레지스트리이며, ECS 배포 흐름에서 중요한 기반 리소스다.
&lt;/p&gt;</description>
      <category>테라폼</category>
      <author>pininini</author>
      <guid isPermaLink="true">https://pininininfra.tistory.com/21</guid>
      <comments>https://pininininfra.tistory.com/21#entry21comment</comments>
      <pubDate>Thu, 14 May 2026 19:09:32 +0900</pubDate>
    </item>
    <item>
      <title>4-8. 테라폼 - Secrets Manager와 SSM Parameter Store 구현하기</title>
      <link>https://pininininfra.tistory.com/20</link>
      <description>&lt;h1 style=&quot;text-align:center;&quot;&gt;테라폼 - Secrets Manager와 SSM Parameter Store 구현하기&lt;/h1&gt;
&lt;p style=&quot;text-align:center;&quot;&gt;&lt;em&gt;민감정보와 운영 설정값을 코드에서 분리해서 관리하기&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;
이전 글에서는 Terraform으로 RDS MySQL을 구현하는 방법을 정리했다.
&lt;/p&gt;

&lt;p&gt;
이번 글에서는 애플리케이션 운영에서 자주 필요한 &lt;strong&gt;Secrets Manager&lt;/strong&gt;와 &lt;strong&gt;SSM Parameter Store&lt;/strong&gt;를 Terraform으로 구현해보려 한다.
&lt;/p&gt;

&lt;p&gt;
서비스를 만들다 보면 다음과 같은 값들이 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DB password
API key
JWT secret
OAuth client secret
외부 서비스 token
DB endpoint
환경별 설정값
feature flag
log level&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이런 값을 모두 코드에 직접 작성하면 위험하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;application.yml
.env
terraform.tfvars
Dockerfile
GitHub repository&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
위와 같은 곳에 비밀번호나 토큰을 직접 넣으면 Git에 올라가거나, Terraform state에 남거나, 로그에 노출될 수 있다.
&lt;/p&gt;

&lt;blockquote&gt;
  민감정보와 설정값은 애플리케이션 코드에서 분리해서 관리해야 한다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;목차&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;1. 왜 Secret과 설정값을 분리해야 할까?&lt;/li&gt;
  &lt;li&gt;2. Secrets Manager란 무엇인가&lt;/li&gt;
  &lt;li&gt;3. SSM Parameter Store란 무엇인가&lt;/li&gt;
  &lt;li&gt;4. Secrets Manager와 Parameter Store 차이&lt;/li&gt;
  &lt;li&gt;5. Terraform으로 Secret 메타데이터 만들기&lt;/li&gt;
  &lt;li&gt;6. Terraform으로 Secret 값을 넣을 때의 주의점&lt;/li&gt;
  &lt;li&gt;7. SSM Parameter Store 구현하기&lt;/li&gt;
  &lt;li&gt;8. IAM 권한 부여하기&lt;/li&gt;
  &lt;li&gt;9. ECS에서 Secret과 Parameter 사용하기&lt;/li&gt;
  &lt;li&gt;10. Lambda에서 Secret과 Parameter 사용하기&lt;/li&gt;
  &lt;li&gt;11. EC2에서 Secret과 Parameter 사용하기&lt;/li&gt;
  &lt;li&gt;12. 실전 예제: DB 접속 정보 관리&lt;/li&gt;
  &lt;li&gt;13. 의존성 흐름&lt;/li&gt;
  &lt;li&gt;14. 자주 하는 실수&lt;/li&gt;
  &lt;li&gt;15. 마무리&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2&gt;1. 왜 Secret과 설정값을 분리해야 할까?&lt;/h2&gt;

&lt;p&gt;
애플리케이션에는 코드와 함께 관리하면 안 되는 값들이 있다.
&lt;/p&gt;

&lt;p&gt;
대표적인 것이 비밀번호와 API Key다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DB_PASSWORD=...
JWT_SECRET=...
OAUTH_CLIENT_SECRET=...
PAYMENT_API_KEY=...&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이런 값들은 외부에 노출되면 바로 보안 사고로 이어질 수 있다.
&lt;/p&gt;

&lt;p&gt;
반대로 민감정보는 아니지만 환경마다 달라지는 설정값도 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;APP_ENV=prod
LOG_LEVEL=info
DB_HOST=demo-mysql.xxxxxx.ap-northeast-2.rds.amazonaws.com
CACHE_TTL=300
FEATURE_NEW_UI=true&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 값들은 비밀번호처럼 민감하지는 않더라도 코드에 하드코딩하면 환경별 관리가 어려워진다.
&lt;/p&gt;

&lt;p&gt;
그래서 값을 다음처럼 분리해서 생각하는 것이 좋다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;구분&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;예시&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;권장 저장 위치&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;민감정보&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;DB password, API key, token&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Secrets Manager&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;일반 설정값&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;log level, endpoint, feature flag&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;SSM Parameter Store&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;민감 설정값&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;간단한 password, internal token&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;SSM SecureString 또는 Secrets Manager&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
정리하면 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;코드
→ 로직

Secrets Manager / Parameter Store
→ 값&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;2. Secrets Manager란 무엇인가&lt;/h2&gt;

&lt;p&gt;
Secrets Manager는 AWS에서 제공하는 secret 관리 서비스다.
&lt;/p&gt;

&lt;p&gt;
다음과 같은 값을 저장할 때 사용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;DB credential
API key
OAuth secret
JWT secret
외부 서비스 token&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Secrets Manager의 핵심은 단순 저장이 아니라 secret의 생명주기 관리다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;저장
조회
권한 제어
버전 관리
자동 rotation
감사 로그&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
특히 DB password처럼 주기적으로 변경해야 하는 값은 Secrets Manager가 더 적합하다.
&lt;/p&gt;

&lt;p&gt;
예를 들어 RDS의 master password를 Secrets Manager로 관리하면 Terraform 코드에 DB password를 직접 넣지 않아도 된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;RDS
→ manage_master_user_password = true
→ Secrets Manager에 master password 저장&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Secrets Manager는 강력하지만 비용이 발생한다.
&lt;/p&gt;

&lt;p&gt;
따라서 모든 설정값을 Secrets Manager에 넣기보다는 정말 secret으로 관리해야 하는 값을 넣는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;3. SSM Parameter Store란 무엇인가&lt;/h2&gt;

&lt;p&gt;
SSM Parameter Store는 AWS Systems Manager의 기능 중 하나다.
&lt;/p&gt;

&lt;p&gt;
애플리케이션 설정값을 이름 기반으로 저장하고 조회할 수 있다.
&lt;/p&gt;

&lt;p&gt;
Parameter Store는 보통 다음과 같은 값을 관리할 때 사용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;환경별 endpoint
log level
feature flag
외부 API URL
AMI ID
간단한 설정값
일부 SecureString 값&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Parameter Store는 이름을 계층형으로 만들 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/demo/dev/db/host
/demo/dev/app/log-level
/demo/prod/db/host
/demo/prod/app/log-level&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 구조를 사용하면 환경별 설정을 깔끔하게 나눌 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/project/env/category/name&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Parameter Store의 타입은 대표적으로 다음 세 가지가 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;String
StringList
SecureString&lt;/code&gt;&lt;/pre&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;타입&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;의미&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;예시&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;String&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;일반 문자열&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;log level, endpoint&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;StringList&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;쉼표로 구분되는 문자열 목록&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;allowed origins, subnet id 목록&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;SecureString&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;KMS로 암호화되는 문자열&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;간단한 password, token&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
초보 단계에서는 이렇게 나누면 이해하기 쉽다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;자주 회전해야 하는 민감정보
→ Secrets Manager

환경 설정값
→ Parameter Store

간단한 민감 설정값
→ Parameter Store SecureString 또는 Secrets Manager&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;4. Secrets Manager와 Parameter Store 차이&lt;/h2&gt;

&lt;p&gt;
Secrets Manager와 Parameter Store는 비슷해 보이지만 목적이 조금 다르다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;구분&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;Secrets Manager&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;SSM Parameter Store&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;주 목적&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;민감정보 관리&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;설정값 관리&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;대표 값&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;DB password, API key, token&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;endpoint, log level, feature flag&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Rotation&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;지원&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;직접 구현 필요&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;계층형 이름&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;가능&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;계층형 설정 관리에 적합&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;사용 예&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;RDS credential, OAuth secret&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;/project/prod/app/log-level&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
절대적인 정답은 없다.
&lt;/p&gt;

&lt;p&gt;
다만 일반적인 기준은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;비밀번호, 토큰, API Key
→ Secrets Manager

환경별 일반 설정값
→ Parameter Store

암호화는 필요하지만 rotation까지 필요 없는 값
→ Parameter Store SecureString 고려&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
  Secrets Manager는 secret의 생명주기 관리에 강하고, Parameter Store는 환경별 설정값 관리에 강하다.
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2&gt;5. Terraform으로 Secret 메타데이터 만들기&lt;/h2&gt;

&lt;p&gt;
Secrets Manager는 보통 두 가지 리소스로 나누어 볼 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;aws_secretsmanager_secret
→ Secret 메타데이터

aws_secretsmanager_secret_version
→ Secret 실제 값&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
먼저 Secret 메타데이터만 만들 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;variable &quot;project_name&quot; {
  description = &quot;Project name&quot;
  type        = string
}

variable &quot;environment&quot; {
  description = &quot;Environment name&quot;
  type        = string
}

resource &quot;aws_secretsmanager_secret&quot; &quot;db&quot; {
  name        = &quot;/${var.project_name}/${var.environment}/db/credential&quot;
  description = &quot;Database credential for ${var.project_name} ${var.environment}&quot;

  recovery_window_in_days = 7

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-db-credential&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 리소스는 Secret의 이름과 설명, 태그를 만든다.
&lt;/p&gt;

&lt;p&gt;
하지만 이 코드만으로는 실제 secret 값이 들어가지는 않는다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Secret 메타데이터 생성
→ 값은 아직 없음&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
실무에서는 Terraform으로 Secret 이름과 접근 권한 구조만 만들고,
실제 값은 AWS Console, AWS CLI, CI/CD, 또는 별도 secret 관리 절차로 주입하는 방식도 많이 사용한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;6. Terraform으로 Secret 값을 넣을 때의 주의점&lt;/h2&gt;

&lt;p&gt;
Terraform으로 Secret 값을 직접 넣을 수도 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;variable &quot;db_username&quot; {
  description = &quot;Database username&quot;
  type        = string
}

variable &quot;db_password&quot; {
  description = &quot;Database password&quot;
  type        = string
  sensitive   = true
}

resource &quot;aws_secretsmanager_secret_version&quot; &quot;db&quot; {
  secret_id = aws_secretsmanager_secret.db.id

  secret_string = jsonencode({
    username = var.db_username
    password = var.db_password
    host     = var.db_host
    port     = 3306
    dbname   = var.db_name
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
하지만 이 방식은 주의해야 한다.
&lt;/p&gt;

&lt;blockquote&gt;
  Terraform으로 secret 값을 만들면 그 값이 Terraform state에 남을 수 있다.
&lt;/blockquote&gt;

&lt;p&gt;
&lt;code&gt;sensitive = true&lt;/code&gt;는 CLI 출력에서 값을 숨기는 데 도움이 되지만,
state에 값이 저장되는 문제를 완전히 없애지는 않는다.
&lt;/p&gt;

&lt;p&gt;
따라서 운영 환경에서는 다음 원칙을 고려하는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Secret 메타데이터
→ Terraform으로 생성 가능

Secret 실제 값
→ Terraform 밖에서 주입하는 방식 고려

Terraform state
→ 민감정보로 취급하고 접근 권한 제한&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
최신 Terraform과 Provider에서는 일부 리소스에서 state에 저장하지 않는 write-only 인자를 지원하기도 한다.
다만 프로젝트에서 사용하는 Terraform 버전과 AWS Provider 버전에 따라 지원 여부가 다르므로, 실제 적용 전 문서를 확인해야 한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;7. SSM Parameter Store 구현하기&lt;/h2&gt;

&lt;p&gt;
이번에는 SSM Parameter Store를 Terraform으로 만들어보자.
&lt;/p&gt;

&lt;p&gt;
일반 설정값은 &lt;code&gt;String&lt;/code&gt; 타입으로 만들 수 있다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;7.1 String Parameter&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ssm_parameter&quot; &quot;log_level&quot; {
  name        = &quot;/${var.project_name}/${var.environment}/app/log-level&quot;
  description = &quot;Application log level&quot;
  type        = &quot;String&quot;
  value       = &quot;info&quot;

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-log-level&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 값은 애플리케이션에서 log level 설정으로 사용할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/demo/prod/app/log-level
→ info&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;7.2 StringList Parameter&lt;/h3&gt;

&lt;p&gt;
여러 값을 목록처럼 저장하고 싶다면 &lt;code&gt;StringList&lt;/code&gt;를 사용할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ssm_parameter&quot; &quot;allowed_origins&quot; {
  name        = &quot;/${var.project_name}/${var.environment}/app/allowed-origins&quot;
  description = &quot;Allowed CORS origins&quot;
  type        = &quot;StringList&quot;
  value       = &quot;https://example.com,https://admin.example.com&quot;

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-allowed-origins&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
StringList는 쉼표로 구분된 문자열로 저장된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;https://example.com,https://admin.example.com&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;7.3 SecureString Parameter&lt;/h3&gt;

&lt;p&gt;
암호화가 필요한 값은 &lt;code&gt;SecureString&lt;/code&gt;으로 만들 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;variable &quot;external_api_token&quot; {
  description = &quot;External API token&quot;
  type        = string
  sensitive   = true
}

resource &quot;aws_ssm_parameter&quot; &quot;api_token&quot; {
  name        = &quot;/${var.project_name}/${var.environment}/app/api-token&quot;
  description = &quot;External API token&quot;
  type        = &quot;SecureString&quot;
  value       = var.external_api_token

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-api-token&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
하지만 SecureString도 Terraform으로 값을 직접 넣으면 state에 평문 값이 남을 수 있다.
&lt;/p&gt;

&lt;p&gt;
따라서 운영 환경에서는 SecureString 값도 Terraform 밖에서 주입하는 방식을 고려하는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;권장 방향:
Terraform → Parameter 이름과 권한 구조 관리
실제 값 → Console / CLI / CI/CD / 별도 secret 관리 절차로 주입&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;8. IAM 권한 부여하기&lt;/h2&gt;

&lt;p&gt;
Secrets Manager나 Parameter Store에 값을 저장했다고 해서 애플리케이션이 자동으로 읽을 수 있는 것은 아니다.
&lt;/p&gt;

&lt;p&gt;
애플리케이션이 실행되는 Role에 읽기 권한을 부여해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EC2 Role
ECS Task Role
ECS Task Execution Role
Lambda Role&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;8.1 Secrets Manager 읽기 권한&lt;/h3&gt;

&lt;p&gt;
특정 Secret을 읽는 권한은 다음처럼 줄 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_policy&quot; &quot;read_db_secret&quot; {
  name = &quot;${var.project_name}-${var.environment}-read-db-secret&quot;

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;secretsmanager:GetSecretValue&quot;
        ]
        Resource = aws_secretsmanager_secret.db.arn
      }
    ]
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;8.2 SSM Parameter 읽기 권한&lt;/h3&gt;

&lt;p&gt;
Parameter Store 값을 읽으려면 다음 권한이 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ssm:GetParameter
ssm:GetParameters
ssm:GetParametersByPath&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
예를 들어 특정 경로 아래의 Parameter를 읽을 수 있도록 할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_policy&quot; &quot;read_app_parameters&quot; {
  name = &quot;${var.project_name}-${var.environment}-read-app-parameters&quot;

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;ssm:GetParameter&quot;,
          &quot;ssm:GetParameters&quot;,
          &quot;ssm:GetParametersByPath&quot;
        ]
        Resource = &quot;arn:aws:ssm:${var.aws_region}:${var.aws_account_id}:parameter/${var.project_name}/${var.environment}/*&quot;
      }
    ]
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
SecureString을 customer managed KMS key로 암호화했다면 &lt;code&gt;kms:Decrypt&lt;/code&gt; 권한도 필요할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SSM SecureString 읽기
→ ssm:GetParameter
→ kms:Decrypt 필요 가능&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;9. ECS에서 Secret과 Parameter 사용하기&lt;/h2&gt;

&lt;p&gt;
ECS는 Secrets Manager나 SSM Parameter Store 값을 컨테이너 환경 변수로 주입할 수 있다.
&lt;/p&gt;

&lt;p&gt;
일반 환경 변수는 &lt;code&gt;environment&lt;/code&gt;에 넣고, 민감정보는 &lt;code&gt;secrets&lt;/code&gt; 블록을 사용하는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;environment
→ 일반 설정값

secrets
→ 민감정보&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;9.1 ECS Task Execution Role 권한&lt;/h3&gt;

&lt;p&gt;
ECS가 컨테이너 실행 시점에 Secret 값을 가져와 환경 변수로 주입하려면 Task Execution Role에 권한이 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;ecs_task_execution&quot; {
  name = &quot;${var.project_name}-${var.environment}-ecs-task-execution-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ecs-tasks.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;ecs_task_execution_default&quot; {
  role       = aws_iam_role.ecs_task_execution.name
  policy_arn = &quot;arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Secrets Manager 값을 주입하려면 추가로 Secret 읽기 권한을 붙일 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_policy&quot; &quot;ecs_execution_read_secrets&quot; {
  name = &quot;${var.project_name}-${var.environment}-ecs-execution-read-secrets&quot;

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;secretsmanager:GetSecretValue&quot;
        ]
        Resource = aws_secretsmanager_secret.db.arn
      },
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;ssm:GetParameters&quot;,
          &quot;ssm:GetParameter&quot;
        ]
        Resource = &quot;arn:aws:ssm:${var.aws_region}:${var.aws_account_id}:parameter/${var.project_name}/${var.environment}/*&quot;
      }
    ]
  })
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;ecs_execution_read_secrets&quot; {
  role       = aws_iam_role.ecs_task_execution.name
  policy_arn = aws_iam_policy.ecs_execution_read_secrets.arn
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
정리하면, ECS에서 &lt;code&gt;secrets&lt;/code&gt; 블록으로 값을 주입할 때는 ECS Agent가 값을 가져와야 하므로 Task Execution Role 권한이 중요하다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;9.2 ECS Task Role 권한&lt;/h3&gt;

&lt;p&gt;
반면 애플리케이션 코드가 실행 중에 직접 Secrets Manager나 Parameter Store를 조회한다면 Task Role에 권한이 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;ecs_task&quot; {
  name = &quot;${var.project_name}-${var.environment}-ecs-task-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ecs-tasks.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;ecs_task_read_parameters&quot; {
  role       = aws_iam_role.ecs_task.name
  policy_arn = aws_iam_policy.read_app_parameters.arn
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
초보자가 헷갈리기 쉬운 부분은 이 차이다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;Role&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;사용 주체&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;사용 예&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Task Execution Role&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECS가 Task를 실행할 때 사용&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;ECR pull, CloudWatch Logs, secrets 주입&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Task Role&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;컨테이너 안의 애플리케이션이 사용&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;S3 접근, SDK로 Secret 조회&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;hr /&gt;

&lt;h3&gt;9.3 ECS Task Definition 예시&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ecs_task_definition&quot; &quot;app&quot; {
  family                   = &quot;${var.project_name}-${var.environment}-app&quot;
  requires_compatibilities = [&quot;FARGATE&quot;]
  network_mode             = &quot;awsvpc&quot;
  cpu                      = 256
  memory                   = 512

  execution_role_arn = aws_iam_role.ecs_task_execution.arn
  task_role_arn      = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name  = &quot;app&quot;
      image = var.image

      environment = [
        {
          name  = &quot;APP_ENV&quot;
          value = var.environment
        },
        {
          name  = &quot;LOG_LEVEL&quot;
          value = &quot;info&quot;
        }
      ]

      secrets = [
        {
          name      = &quot;DB_CREDENTIAL&quot;
          valueFrom = aws_secretsmanager_secret.db.arn
        },
        {
          name      = &quot;API_TOKEN&quot;
          valueFrom = aws_ssm_parameter.api_token.arn
        }
      ]
    }
  ])
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이렇게 하면 컨테이너 안에서는 다음 환경 변수로 값을 받을 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;APP_ENV
LOG_LEVEL
DB_CREDENTIAL
API_TOKEN&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 secret을 환경 변수로 주입하면 애플리케이션에서 사용하기는 편하지만, 컨테이너 내부 환경 변수 노출이나 로그 출력에 주의해야 한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;10. Lambda에서 Secret과 Parameter 사용하기&lt;/h2&gt;

&lt;p&gt;
Lambda는 보통 실제 secret 값을 환경 변수에 직접 넣기보다, Secret 이름이나 ARN을 환경 변수에 넣고 코드에서 Secrets Manager를 조회하는 방식을 사용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lambda 환경 변수
→ Secret 이름만 저장

Lambda 코드
→ Secrets Manager / Parameter Store 조회&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;10.1 Lambda 실행 Role&lt;/h3&gt;

&lt;p&gt;
Lambda가 Secrets Manager 값을 읽으려면 실행 Role에 권한이 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;lambda&quot; {
  name = &quot;${var.project_name}-${var.environment}-lambda-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;lambda.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;lambda_basic&quot; {
  role       = aws_iam_role.lambda.name
  policy_arn = &quot;arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;10.2 Lambda Secret 읽기 권한&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_policy&quot; &quot;lambda_read_db_secret&quot; {
  name = &quot;${var.project_name}-${var.environment}-lambda-read-db-secret&quot;

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;secretsmanager:GetSecretValue&quot;
        ]
        Resource = aws_secretsmanager_secret.db.arn
      },
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;ssm:GetParameter&quot;,
          &quot;ssm:GetParameters&quot;
        ]
        Resource = &quot;arn:aws:ssm:${var.aws_region}:${var.aws_account_id}:parameter/${var.project_name}/${var.environment}/*&quot;
      }
    ]
  })
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;lambda_read_db_secret&quot; {
  role       = aws_iam_role.lambda.name
  policy_arn = aws_iam_policy.lambda_read_db_secret.arn
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;10.3 Lambda 환경 변수 설정&lt;/h3&gt;

&lt;p&gt;
환경 변수에는 실제 password가 아니라 Secret 이름이나 ARN을 넣는다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_lambda_function&quot; &quot;app&quot; {
  function_name = &quot;${var.project_name}-${var.environment}-app&quot;
  role          = aws_iam_role.lambda.arn

  handler = &quot;index.handler&quot;
  runtime = &quot;nodejs20.x&quot;
  filename = &quot;lambda.zip&quot;

  environment {
    variables = {
      APP_ENV        = var.environment
      DB_SECRET_NAME = aws_secretsmanager_secret.db.name
      LOG_LEVEL_PARAM = aws_ssm_parameter.log_level.name
    }
  }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 구조의 장점은 Lambda 설정에 실제 secret 값이 직접 들어가지 않는다는 점이다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lambda 환경 변수
→ Secret 이름만 저장

Secrets Manager
→ 실제 username / password 저장&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;10.4 Lambda 코드 예시&lt;/h3&gt;

&lt;p&gt;
Node.js Lambda라면 다음처럼 Secret을 조회할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import { SecretsManagerClient, GetSecretValueCommand } from &quot;@aws-sdk/client-secrets-manager&quot;;
import { SSMClient, GetParameterCommand } from &quot;@aws-sdk/client-ssm&quot;;

const secretsClient = new SecretsManagerClient({});
const ssmClient = new SSMClient({});

let cachedDbSecret;
let cachedLogLevel;

async function getDbSecret() {
  if (cachedDbSecret) {
    return cachedDbSecret;
  }

  const response = await secretsClient.send(
    new GetSecretValueCommand({
      SecretId: process.env.DB_SECRET_NAME,
    })
  );

  cachedDbSecret = JSON.parse(response.SecretString);
  return cachedDbSecret;
}

async function getLogLevel() {
  if (cachedLogLevel) {
    return cachedLogLevel;
  }

  const response = await ssmClient.send(
    new GetParameterCommand({
      Name: process.env.LOG_LEVEL_PARAM,
    })
  );

  cachedLogLevel = response.Parameter.Value;
  return cachedLogLevel;
}

export const handler = async (event) =&amp;gt; {
  const dbSecret = await getDbSecret();
  const logLevel = await getLogLevel();

  return {
    statusCode: 200,
    body: JSON.stringify({
      message: &quot;loaded config&quot;,
      dbUser: dbSecret.username,
      logLevel: logLevel,
    }),
  };
};&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
위 코드에서 중요한 점은 Secret과 Parameter를 매 요청마다 무조건 다시 조회하지 않고 메모리에 캐싱한다는 점이다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;첫 번째 호출
→ Secrets Manager / Parameter Store 조회
→ 메모리에 캐싱

이후 같은 Lambda 실행 환경 재사용
→ 캐시된 값 사용&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 Secret rotation을 사용한다면 캐시 만료 시간도 함께 고려해야 한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;10.5 Lambda Extension 방식&lt;/h3&gt;

&lt;p&gt;
Lambda에서는 AWS Parameters and Secrets Lambda Extension을 사용할 수도 있다.
&lt;/p&gt;

&lt;p&gt;
이 방식은 코드에서 SDK를 직접 호출하는 대신, Lambda 실행 환경 내부의 로컬 HTTP 엔드포인트를 통해 Secret이나 Parameter를 조회한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Lambda 코드
→ localhost extension endpoint 호출
→ Secrets Manager / Parameter Store 값 조회&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
초보 단계에서는 먼저 SDK 방식으로 흐름을 이해하고, 이후 성능이나 캐싱을 더 신경 써야 할 때 Extension을 고려해도 충분하다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;11. EC2에서 Secret과 Parameter 사용하기&lt;/h2&gt;

&lt;p&gt;
EC2는 IAM Instance Profile을 통해 Secrets Manager나 Parameter Store를 읽을 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;EC2
→ Instance Profile
→ IAM Role
→ Secrets Manager / Parameter Store 읽기&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;11.1 EC2 IAM Role&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;ec2&quot; {
  name = &quot;${var.project_name}-${var.environment}-ec2-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ec2.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;11.2 EC2 Role에 읽기 권한 부여&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_policy&quot; &quot;ec2_read_config&quot; {
  name = &quot;${var.project_name}-${var.environment}-ec2-read-config&quot;

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;secretsmanager:GetSecretValue&quot;
        ]
        Resource = aws_secretsmanager_secret.db.arn
      },
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;ssm:GetParameter&quot;,
          &quot;ssm:GetParameters&quot;,
          &quot;ssm:GetParametersByPath&quot;
        ]
        Resource = &quot;arn:aws:ssm:${var.aws_region}:${var.aws_account_id}:parameter/${var.project_name}/${var.environment}/*&quot;
      }
    ]
  })
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;ec2_read_config&quot; {
  role       = aws_iam_role.ec2.name
  policy_arn = aws_iam_policy.ec2_read_config.arn
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;11.3 Instance Profile 연결&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_instance_profile&quot; &quot;ec2&quot; {
  name = &quot;${var.project_name}-${var.environment}-ec2-profile&quot;
  role = aws_iam_role.ec2.name
}

resource &quot;aws_instance&quot; &quot;app&quot; {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = &quot;t3.micro&quot;
  subnet_id              = aws_subnet.private.id
  vpc_security_group_ids = [aws_security_group.app.id]

  iam_instance_profile = aws_iam_instance_profile.ec2.name

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-${var.environment}-ec2&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;11.4 EC2 내부에서 AWS CLI로 조회하기&lt;/h3&gt;

&lt;p&gt;
EC2 내부에서는 AWS CLI로 Secret이나 Parameter를 조회할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;aws secretsmanager get-secret-value \
  --secret-id /demo/prod/db/credential&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Parameter Store 값은 다음처럼 조회할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;aws ssm get-parameter \
  --name /demo/prod/app/log-level&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
SecureString 값을 복호화해서 읽으려면 &lt;code&gt;--with-decryption&lt;/code&gt; 옵션을 사용한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;aws ssm get-parameter \
  --name /demo/prod/app/api-token \
  --with-decryption&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;11.5 User Data에서 설정 파일 생성 예시&lt;/h3&gt;

&lt;p&gt;
EC2 부팅 시 User Data에서 Parameter Store 값을 읽어 설정 파일을 만들 수도 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;user_data = &amp;lt;&amp;lt;-EOF
#!/bin/bash

APP_ENV=$(aws ssm get-parameter \
  --name &quot;/demo/prod/app/env&quot; \
  --query &quot;Parameter.Value&quot; \
  --output text)

LOG_LEVEL=$(aws ssm get-parameter \
  --name &quot;/demo/prod/app/log-level&quot; \
  --query &quot;Parameter.Value&quot; \
  --output text)

cat &amp;gt; /etc/app.env &amp;lt;&amp;lt;EOT
APP_ENV=$APP_ENV
LOG_LEVEL=$LOG_LEVEL
EOT
EOF&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 User Data에 secret 값을 파일로 남기는 방식은 주의해야 한다.
&lt;/p&gt;

&lt;p&gt;
비밀번호나 토큰은 파일 권한, 로그 출력, AMI 이미지화 여부까지 함께 고려해야 한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;12. 실전 예제: DB 접속 정보 관리&lt;/h2&gt;

&lt;p&gt;
이번에는 DB 접속 정보를 어떻게 나누어 관리할지 예시로 보자.
&lt;/p&gt;

&lt;p&gt;
DB 접속에는 보통 다음 값들이 필요하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;host
port
dbname
username
password&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이 중에서 모든 값이 같은 수준의 secret은 아니다.
&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto; margin:16px 0;&quot;&gt;
  &lt;table style=&quot;width:100%; border-collapse:collapse; border:1px solid #d9e2ec; font-size:14px;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color:#eef6f1;&quot;&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;값&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;민감도&lt;/th&gt;
        &lt;th style=&quot;border:1px solid #d9e2ec; padding:10px; text-align:left;&quot;&gt;저장 위치 예시&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;host&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;낮음&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;SSM Parameter Store&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;port&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;낮음&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;SSM Parameter Store&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;dbname&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;낮음&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;SSM Parameter Store&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color:#fafafa;&quot;&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;username&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;중간&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Secrets Manager 또는 Parameter Store&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;password&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;높음&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #d9e2ec; padding:10px;&quot;&gt;Secrets Manager&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;
운영에서는 DB credential을 하나의 JSON Secret으로 관리할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  &quot;username&quot;: &quot;admin&quot;,
  &quot;password&quot;: &quot;xxxx&quot;,
  &quot;host&quot;: &quot;demo-mysql.xxxxxx.ap-northeast-2.rds.amazonaws.com&quot;,
  &quot;port&quot;: 3306,
  &quot;dbname&quot;: &quot;appdb&quot;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 RDS에서 &lt;code&gt;manage_master_user_password = true&lt;/code&gt;를 사용한다면 master password는 RDS가 Secrets Manager에 생성한 Secret으로 관리된다.
&lt;/p&gt;

&lt;p&gt;
이 경우 애플리케이션에는 다음 값들을 전달해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;RDS endpoint
DB name
Secret ARN 또는 Secret name&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
예를 들어 RDS endpoint는 Parameter Store에 저장할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_ssm_parameter&quot; &quot;db_host&quot; {
  name  = &quot;/${var.project_name}/${var.environment}/db/host&quot;
  type  = &quot;String&quot;
  value = aws_db_instance.mysql.address
}

resource &quot;aws_ssm_parameter&quot; &quot;db_name&quot; {
  name  = &quot;/${var.project_name}/${var.environment}/db/name&quot;
  type  = &quot;String&quot;
  value = aws_db_instance.mysql.db_name
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
RDS가 관리하는 master password secret ARN은 output으로 전달할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;output &quot;db_host_parameter_name&quot; {
  description = &quot;SSM parameter name for DB host&quot;
  value       = aws_ssm_parameter.db_host.name
}

output &quot;db_secret_arn&quot; {
  description = &quot;RDS master user secret ARN&quot;
  value       = aws_db_instance.mysql.master_user_secret[0].secret_arn
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이렇게 하면 애플리케이션은 Parameter Store에서 host를 읽고, Secrets Manager에서 password를 읽는 구조로 만들 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;App
→ SSM Parameter Store에서 DB host 조회
→ Secrets Manager에서 DB credential 조회
→ RDS 접속&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;13. 의존성 흐름&lt;/h2&gt;

&lt;p&gt;
Secrets Manager와 Parameter Store를 Terraform으로 구현할 때의 의존성 흐름은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Secret / Parameter
→ IAM Policy
→ IAM Role
→ 실행 리소스&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
여기서 실행 리소스는 ECS Task, Lambda Function, EC2 Instance 같은 리소스를 의미한다.
&lt;/p&gt;

&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;30%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpf15j/dJMcagFxdYl/KovPIQ0sk7En20IHY18Ak0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpf15j/dJMcagFxdYl/KovPIQ0sk7En20IHY18Ak0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpf15j/dJMcagFxdYl/KovPIQ0sk7En20IHY18Ak0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcpf15j%2FdJMcagFxdYl%2FKovPIQ0sk7En20IHY18Ak0%2Fimg.png&quot; width=&quot;30%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;


&lt;p&gt;
이 구조에서 중요한 점은 두 가지다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;1. IAM Policy는 Secret / Parameter의 ARN을 참조한다.
2. 실행 리소스는 IAM Role과 Secret / Parameter 이름 또는 ARN을 참조한다.&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2&gt;14. 자주 하는 실수&lt;/h2&gt;

&lt;h3&gt;14.1 Secret 값을 Terraform 코드에 직접 작성함&lt;/h3&gt;

&lt;p&gt;
다음처럼 secret 값을 코드에 직접 쓰면 안 된다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;secret_string = &quot;my-password&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
코드 저장소와 Terraform state에 민감정보가 남을 수 있다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.2 sensitive = true면 state에도 안 남는다고 생각함&lt;/h3&gt;

&lt;p&gt;
&lt;code&gt;sensitive = true&lt;/code&gt;는 화면 출력이나 plan 출력에서 값을 숨기는 데 도움을 준다.
&lt;/p&gt;

&lt;p&gt;
하지만 state 저장 문제를 완전히 없애지는 않는다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sensitive = true
→ 출력 숨김

state
→ 값 저장 가능성 있음&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
따라서 state 자체를 민감한 데이터로 보고 접근 권한을 제한해야 한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.3 SecureString이면 Terraform state도 안전하다고 생각함&lt;/h3&gt;

&lt;p&gt;
SSM Parameter Store의 SecureString은 AWS 안에서 암호화되어 저장된다.
&lt;/p&gt;

&lt;p&gt;
하지만 Terraform으로 SecureString 값을 직접 관리하면 Terraform state에는 평문 값이 남을 수 있다.
&lt;/p&gt;

&lt;p&gt;
따라서 SecureString을 사용하더라도 Terraform state 보안을 함께 고려해야 한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.4 Parameter Store와 Secrets Manager를 구분하지 않고 사용함&lt;/h3&gt;

&lt;p&gt;
모든 값을 Secrets Manager에 넣거나, 모든 값을 Parameter Store에 넣는 방식은 비용과 관리 측면에서 비효율적일 수 있다.
&lt;/p&gt;

&lt;p&gt;
값의 성격에 따라 나누는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;회전이 필요한 credential
→ Secrets Manager

환경별 일반 설정값
→ Parameter Store

간단한 암호화 설정값
→ SecureString 고려&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.5 IAM 권한을 너무 넓게 줌&lt;/h3&gt;

&lt;p&gt;
다음처럼 모든 secret과 parameter를 읽을 수 있게 하는 것은 편하지만 위험하다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Resource = &quot;*&quot;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
가능하면 특정 ARN이나 특정 경로로 제한한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;arn:aws:ssm:ap-northeast-2:123456789012:parameter/demo/prod/*
arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:/demo/prod/*&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.6 ECS에서 secret을 environment에 평문으로 넣음&lt;/h3&gt;

&lt;p&gt;
ECS Task Definition에서 민감정보를 일반 &lt;code&gt;environment&lt;/code&gt;에 직접 넣는 것은 피하는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;environment = [
  {
    name  = &quot;DB_PASSWORD&quot;
    value = &quot;my-password&quot;
  }
]&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
대신 &lt;code&gt;secrets&lt;/code&gt; 블록을 사용해서 Secrets Manager나 Parameter Store 값을 참조한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;secrets = [
  {
    name      = &quot;DB_PASSWORD&quot;
    valueFrom = aws_secretsmanager_secret.db.arn
  }
]&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3&gt;14.7 Lambda 환경 변수에 secret 값을 직접 넣음&lt;/h3&gt;

&lt;p&gt;
Lambda 환경 변수에는 실제 password나 token보다 Secret 이름이나 ARN을 넣는 것이 좋다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;권장하지 않음:
DB_PASSWORD=my-password

권장:
DB_SECRET_NAME=/demo/prod/db/credential&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
이후 Lambda 코드에서 실행 Role 권한으로 Secrets Manager를 조회한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.8 EC2 User Data에 secret 값을 남김&lt;/h3&gt;

&lt;p&gt;
User Data는 EC2 초기 설정에 유용하지만, 민감정보를 직접 넣는 것은 위험할 수 있다.
&lt;/p&gt;

&lt;p&gt;
User Data 내용이 로그나 메타데이터 조회 과정에서 노출될 수 있기 때문이다.
&lt;/p&gt;

&lt;p&gt;
따라서 User Data에는 secret 값 자체보다 Secret 이름이나 Parameter 이름을 넣고, 실행 시점에 IAM Role로 조회하는 방식이 더 안전하다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.9 Secret rotation을 고려하지 않음&lt;/h3&gt;

&lt;p&gt;
Secrets Manager를 사용하는 이유 중 하나는 rotation이다.
&lt;/p&gt;

&lt;p&gt;
DB password나 외부 API token처럼 주기적으로 변경해야 하는 값은 rotation 전략을 함께 고려해야 한다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Secret 저장
→ 접근 권한 부여
→ 애플리케이션 사용
→ rotation 전략 수립&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
다만 모든 secret에 자동 rotation이 필요한 것은 아니다.
&lt;/p&gt;

&lt;p&gt;
rotation이 필요한 값과 그렇지 않은 값을 구분해서 설계하는 것이 좋다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3&gt;14.10 삭제된 Secret이 바로 없어졌다고 생각함&lt;/h3&gt;

&lt;p&gt;
Secrets Manager Secret은 삭제 시 즉시 완전히 사라지는 것이 아니라 복구 가능한 기간을 둘 수 있다.
&lt;/p&gt;

&lt;p&gt;
Terraform에서는 다음처럼 복구 기간을 설정할 수 있다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;aws_secretsmanager_secret&quot; &quot;db&quot; {
  name                    = &quot;/demo/prod/db/credential&quot;
  recovery_window_in_days = 7
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
학습용에서는 즉시 삭제가 편할 수 있지만, 운영 환경에서는 복구 기간을 두는 것이 안전할 수 있다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h2&gt;15. 마무리&lt;/h2&gt;

&lt;p&gt;
이번 글에서는 Terraform으로 Secrets Manager와 SSM Parameter Store를 구현하는 방법을 정리했다.
&lt;/p&gt;

&lt;p&gt;
핵심은 다음과 같다.
&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Secrets Manager
→ 비밀번호, 토큰, API Key 같은 민감정보 관리

SSM Parameter Store
→ 환경별 설정값과 간단한 SecureString 관리

IAM
→ 애플리케이션이 secret과 parameter를 읽을 권한 제어

ECS
→ Task Definition의 secrets 블록으로 주입 가능

Lambda
→ 환경 변수에는 Secret 이름을 넣고 코드에서 조회

EC2
→ Instance Profile 권한으로 CLI 또는 애플리케이션에서 조회

Terraform state
→ 민감정보가 남을 수 있으므로 반드시 보호 필요&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;
Secret 관리에서 가장 중요한 것은 단순히 어디에 저장하느냐가 아니다.
&lt;/p&gt;

&lt;p&gt;
누가 읽을 수 있는지, 어떻게 회전할지, state에 값이 남지 않는지, 애플리케이션에 어떻게 전달할지를 함께 봐야 한다.
&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 style=&quot;text-align:center;&quot;&gt;한 줄 정리&lt;/h3&gt;

&lt;p style=&quot;text-align:center;&quot;&gt;
&lt;strong&gt;Secrets Manager와 Parameter Store는 값을 코드에서 분리하고, IAM으로 읽기 권한을 제어하며, 실행 환경에 맞는 방식으로 전달하기 위한 도구다.&lt;/strong&gt;
&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;
다음 글에서는 ALB와 Target Group을 Terraform으로 구현해본다.
ALB는 외부 요청을 받아 애플리케이션 서버로 전달하고, Target Group Health Check를 통해 정상 대상에게만 트래픽을 보내는 핵심 리소스다.
&lt;/p&gt;</description>
      <category>테라폼</category>
      <author>pininini</author>
      <guid isPermaLink="true">https://pininininfra.tistory.com/20</guid>
      <comments>https://pininininfra.tistory.com/20#entry20comment</comments>
      <pubDate>Wed, 13 May 2026 17:28:40 +0900</pubDate>
    </item>
    <item>
      <title>4-7. 테라폼 - RDS MySQL 구현하기</title>
      <link>https://pininininfra.tistory.com/19</link>
      <description>&lt;h1 style=&quot;text-align: center;&quot;&gt;테라폼 - RDS MySQL 구현하기&lt;/h1&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;DB Subnet Group, Security Group, 백업, 삭제 보호, 비밀번호 관리까지 함께 보기&lt;/i&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 Terraform으로 S3를 구현하는 방법을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 AWS의 관리형 데이터베이스 서비스인 &lt;b&gt;RDS MySQL&lt;/b&gt;을 Terraform으로 구현해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 EC2에 직접 MySQL을 설치하는 방식과 다르게, AWS가 데이터베이스 운영에 필요한 많은 부분을 관리해주는 서비스다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;DB 인스턴스 생성
스토리지 관리
백업
스냅샷
패치
모니터링
Multi-AZ 구성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Terraform으로 RDS를 만들 때는 단순히 &lt;code&gt;aws_db_instance&lt;/code&gt; 하나만 작성하면 끝나는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 네트워크, 보안, 백업, 삭제 보호, 비밀번호 관리까지 함께 고려해야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;RDS는 생성보다 삭제와 비밀번호 관리가 더 중요하다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. RDS란 무엇인가&lt;/li&gt;
&lt;li&gt;2. RDS를 만들 때 필요한 요소&lt;/li&gt;
&lt;li&gt;3. RDS는 어디에 배치해야 할까?&lt;/li&gt;
&lt;li&gt;4. DB Subnet Group 만들기&lt;/li&gt;
&lt;li&gt;5. RDS Security Group 만들기&lt;/li&gt;
&lt;li&gt;6. 기본 RDS MySQL 생성 코드&lt;/li&gt;
&lt;li&gt;7. 비밀번호 관리 방식&lt;/li&gt;
&lt;li&gt;8. 백업과 스냅샷 설정&lt;/li&gt;
&lt;li&gt;9. 삭제 보호 설정&lt;/li&gt;
&lt;li&gt;10. 실전 예제: Private RDS MySQL 구성&lt;/li&gt;
&lt;li&gt;11. RDS 의존성 흐름&lt;/li&gt;
&lt;li&gt;12. 자주 하는 실수&lt;/li&gt;
&lt;li&gt;13. 마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. RDS란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 Relational Database Service의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에서 제공하는 관리형 관계형 데이터베이스 서비스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 EC2에 MySQL을 설치해서 운영할 수도 있지만, 그렇게 하면 다음 작업을 모두 직접 관리해야 한다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;DB 설치
패치
백업
장애 대응
스토리지 확장
모니터링
복구&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS를 사용하면 이런 운영 작업 중 상당 부분을 AWS 관리형 서비스로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 여러 데이터베이스 엔진을 지원한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;MySQL
PostgreSQL
MariaDB
Oracle
SQL Server
Db2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 초보자가 많이 사용하는 &lt;b&gt;RDS MySQL&lt;/b&gt; 기준으로 설명한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. RDS를 만들 때 필요한 요소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform으로 RDS를 만들 때는 다음 요소들을 함께 생각해야 한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;요소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB Subnet Group&lt;/td&gt;
&lt;td&gt;RDS가 배치될 Subnet 묶음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Group&lt;/td&gt;
&lt;td&gt;DB 접속을 허용할 네트워크 규칙&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB Instance&lt;/td&gt;
&lt;td&gt;실제 RDS 인스턴스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Username / Password&lt;/td&gt;
&lt;td&gt;DB master 계정 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backup&lt;/td&gt;
&lt;td&gt;자동 백업 보존 기간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot&lt;/td&gt;
&lt;td&gt;삭제 시 최종 백업 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deletion Protection&lt;/td&gt;
&lt;td&gt;실수로 DB가 삭제되지 않도록 보호&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, RDS는 다음 리소스와 함께 구성하는 것이 일반적이다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;aws_db_subnet_group
aws_security_group
aws_vpc_security_group_ingress_rule
aws_db_instance&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. RDS는 어디에 배치해야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 일반적으로 &lt;b&gt;Private Subnet&lt;/b&gt;에 배치하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB는 외부 인터넷에서 직접 접근할 필요가 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 경우 애플리케이션 서버만 DB에 접근하면 된다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;User
&amp;rarr; ALB
&amp;rarr; EC2 또는 ECS
&amp;rarr; RDS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 RDS는 Public Subnet이 아니라 Private Subnet에 배치하고, Security Group으로 접근 대상을 제한하는 것이 안전하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;권장 구조:
App Security Group &amp;rarr; RDS Security Group &amp;rarr; 3306 포트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 다음 구조는 피하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Internet &amp;rarr; RDS 3306 직접 접근&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자 실습 중에도 RDS를 public으로 열어두는 습관은 좋지 않다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;RDS는 기본적으로 Private Subnet에 두고, 애플리케이션 Security Group에서만 접근하도록 제한하는 것이 좋다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. DB Subnet Group 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 EC2처럼 단일 Subnet 하나를 직접 지정하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 &lt;b&gt;DB Subnet Group&lt;/b&gt;을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB Subnet Group은 RDS가 사용할 Subnet 목록을 묶어둔 리소스다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Private Subnet A
Private Subnet C
&amp;rarr; DB Subnet Group
&amp;rarr; RDS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_db_subnet_group&quot; &quot;main&quot; {
  name       = &quot;${var.project_name}-db-subnet-group&quot;
  subnet_ids = var.private_subnet_ids

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-db-subnet-group&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;private_subnet_ids&lt;/code&gt;는 Private Subnet ID 목록이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;private_subnet_ids&quot; {
  description = &quot;Private subnet IDs for RDS&quot;
  type        = list(string)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 가능하면 서로 다른 AZ의 Private Subnet을 두 개 이상 사용하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ap-northeast-2a private subnet
ap-northeast-2c private subnet&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 Multi-AZ 구성이나 장애 대응 측면에서 더 유리하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. RDS Security Group 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS Security Group은 DB에 누가 접근할 수 있는지를 제어한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 기본적으로 3306 포트를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS를 전체 인터넷에 열어두면 위험하다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;# 권장하지 않음
0.0.0.0/0 &amp;rarr; RDS 3306&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 애플리케이션 Security Group에서만 접근하도록 제한한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;App Security Group
&amp;rarr; RDS Security Group
&amp;rarr; 3306 포트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;rds&quot; {
  name        = &quot;${var.project_name}-rds-sg&quot;
  description = &quot;Security group for RDS MySQL&quot;
  vpc_id      = var.vpc_id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-rds-sg&quot;
  })
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;rds_from_app&quot; {
  security_group_id = aws_security_group.rds.id

  referenced_security_group_id = var.app_security_group_id
  ip_protocol                  = &quot;tcp&quot;
  from_port                    = 3306
  to_port                      = 3306

  description = &quot;Allow MySQL access from app security group&quot;
}

resource &quot;aws_vpc_security_group_egress_rule&quot; &quot;rds_all&quot; {
  security_group_id = aws_security_group.rds.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;-1&quot;

  description = &quot;Allow all outbound traffic&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 부분은 &lt;code&gt;referenced_security_group_id&lt;/code&gt;다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;referenced_security_group_id = var.app_security_group_id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 특정 IP가 아니라 App Security Group을 가진 리소스만 RDS에 접근할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, IP 중심이 아니라 역할 중심의 보안 규칙이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 기본 RDS MySQL 생성 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기본적인 RDS MySQL 인스턴스를 만들어보자.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_db_instance&quot; &quot;mysql&quot; {
  identifier = &quot;${var.project_name}-mysql&quot;

  engine         = &quot;mysql&quot;
  engine_version = &quot;8.0&quot;
  instance_class = &quot;db.t3.micro&quot;

  allocated_storage     = 20
  max_allocated_storage = 100
  storage_type          = &quot;gp3&quot;
  storage_encrypted     = true

  db_name  = var.db_name
  username = var.db_username
  password = var.db_password

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  publicly_accessible = false

  backup_retention_period = 7
  deletion_protection     = true
  skip_final_snapshot     = false
  final_snapshot_identifier = &quot;${var.project_name}-mysql-final-snapshot&quot;

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-mysql&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 설정을 정리하면 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;설정&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;engine&lt;/td&gt;
&lt;td&gt;DB 엔진&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;engine_version&lt;/td&gt;
&lt;td&gt;DB 엔진 버전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;instance_class&lt;/td&gt;
&lt;td&gt;DB 인스턴스 사양&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;allocated_storage&lt;/td&gt;
&lt;td&gt;초기 스토리지 크기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max_allocated_storage&lt;/td&gt;
&lt;td&gt;자동 스토리지 확장 최대값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;storage_encrypted&lt;/td&gt;
&lt;td&gt;스토리지 암호화 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db_subnet_group_name&lt;/td&gt;
&lt;td&gt;RDS가 배치될 Subnet Group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vpc_security_group_ids&lt;/td&gt;
&lt;td&gt;RDS에 연결할 Security Group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;publicly_accessible&lt;/td&gt;
&lt;td&gt;외부 공개 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;backup_retention_period&lt;/td&gt;
&lt;td&gt;자동 백업 보존 기간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deletion_protection&lt;/td&gt;
&lt;td&gt;삭제 보호 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;skip_final_snapshot&lt;/td&gt;
&lt;td&gt;삭제 시 최종 스냅샷 생략 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 비밀번호 관리 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS를 만들 때 가장 조심해야 하는 부분 중 하나가 비밀번호다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자는 다음처럼 변수로 비밀번호를 넣기 쉽다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;db_password&quot; {
  description = &quot;Database password&quot;
  type        = string
  sensitive   = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 RDS에서 사용한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;password = var.db_password&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 간단하지만 주의할 점이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Terraform 변수에 sensitive = true를 붙여도 값이 state에 저장될 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, CLI 출력에서는 숨겨져도 Terraform state에는 민감한 값이 남을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 운영 환경에서는 RDS master password를 코드나 tfvars에 직접 넣는 방식은 피하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 방법 1: Terraform 변수로 비밀번호 전달&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습용 또는 간단한 실습에서는 변수로 비밀번호를 전달할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;db_password&quot; {
  description = &quot;Database master password&quot;
  type        = string
  sensitive   = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;resource &quot;aws_db_instance&quot; &quot;mysql&quot; {
  username = var.db_username
  password = var.db_password
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 방식은 운영 환경에서는 신중해야 한다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;장점:
- 단순하다.
- 이해하기 쉽다.

단점:
- state에 민감정보가 남을 수 있다.
- tfvars 관리에 주의해야 한다.
- 비밀번호 회전 관리가 어렵다.&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 방법 2: RDS가 Secrets Manager로 master password 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에는 RDS가 master user password를 AWS Secrets Manager로 관리하도록 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서는 다음처럼 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_db_instance&quot; &quot;mysql&quot; {
  identifier = &quot;${var.project_name}-mysql&quot;

  engine         = &quot;mysql&quot;
  instance_class = &quot;db.t3.micro&quot;

  allocated_storage = 20

  db_name  = var.db_name
  username = var.db_username

  manage_master_user_password = true

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  publicly_accessible = false
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에서는 Terraform 코드에 master password를 직접 넣지 않는다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;password 설정 X
manage_master_user_password = true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS가 Secrets Manager에 Secret을 생성하고 master password를 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 운영 환경에서 더 안전한 선택이 될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;장점:
- Terraform 코드에 DB password를 직접 넣지 않는다.
- RDS와 Secrets Manager를 통해 credential을 관리할 수 있다.
- 비밀번호 회전과 관리 측면에서 유리하다.

주의:
- Secrets Manager 비용이 발생할 수 있다.
- 애플리케이션이 Secret을 읽으려면 IAM 권한이 필요하다.&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 애플리케이션에서 비밀번호 읽기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS 비밀번호를 Secrets Manager에서 관리한다면 애플리케이션은 Secret을 읽을 권한이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 ECS Task나 EC2 Role에 다음 권한이 필요할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;secretsmanager:GetSecretValue&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, RDS 접속에는 세 가지가 함께 필요하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. 네트워크 접근
   App Security Group &amp;rarr; RDS Security Group 3306

2. Secret 접근 권한
   IAM Role &amp;rarr; secretsmanager:GetSecretValue

3. DB 로그인 정보
   username / password&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group만 맞다고 DB 접속이 되는 것이 아니고, IAM 권한만 있다고 DB 포트에 접근할 수 있는 것도 아니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 백업과 스냅샷 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 데이터베이스이기 때문에 백업 설정이 매우 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서 자동 백업 보존 기간은 다음 설정으로 지정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;backup_retention_period = 7&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값은 자동 백업을 며칠 동안 보관할지 의미한다.&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;0  &amp;rarr; 자동 백업 비활성화
7  &amp;rarr; 7일간 자동 백업 보관
30 &amp;rarr; 30일간 자동 백업 보관&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 일반적으로 &lt;code&gt;0&lt;/code&gt;으로 두지 않는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 삭제 시 final snapshot&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS를 삭제할 때는 최종 스냅샷을 남길지 결정해야 한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;skip_final_snapshot = false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 삭제 시 최종 스냅샷을 생략하지 않겠다는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 final snapshot identifier도 지정해야 한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;final_snapshot_identifier = &quot;${var.project_name}-mysql-final-snapshot&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 학습용 RDS에서는 빠르게 삭제하기 위해 다음처럼 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;skip_final_snapshot = true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 운영 환경에서는 매우 위험할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;학습용:
skip_final_snapshot = true 가능

운영:
skip_final_snapshot = false 권장&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;RDS를 삭제할 때 final snapshot을 남기지 않으면 삭제 후 복구할 수 있는 지점이 줄어든다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 backup_window와 maintenance_window&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 백업 시간과 유지보수 시간도 고려해야 한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;backup_window      = &quot;18:00-19:00&quot;
maintenance_window = &quot;sun:19:00-sun:20:00&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UTC 기준으로 설정되므로 한국 시간과 차이를 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 한국 시간 새벽에 백업이 돌도록 하고 싶다면 UTC 변환을 고려해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 삭제 보호 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 실수로 삭제되면 큰 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 삭제 보호를 적극적으로 고려해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.1 deletion_protection&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS 자체에는 &lt;code&gt;deletion_protection&lt;/code&gt; 설정이 있다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;deletion_protection = true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정이 켜져 있으면 RDS 삭제가 보호된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 RDS에서는 가능한 켜두는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.2 Terraform lifecycle prevent_destroy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 레벨에서도 삭제를 막을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;resource &quot;aws_db_instance&quot; &quot;mysql&quot; {
  identifier = &quot;${var.project_name}-mysql&quot;

  lifecycle {
    prevent_destroy = true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 Terraform이 해당 리소스를 삭제하려고 할 때 오류를 발생시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, RDS 보호는 두 단계로 생각할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;AWS RDS 레벨:
deletion_protection = true

Terraform 레벨:
prevent_destroy = true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 둘 다 고려할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;중요한 DB는 AWS 서비스 레벨과 Terraform 레벨에서 모두 삭제를 방지하는 것이 좋다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 실전 예제: Private RDS MySQL 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실전형 예제를 하나로 정리해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Private Subnet
&amp;rarr; DB Subnet Group
&amp;rarr; RDS MySQL

App Security Group
&amp;rarr; RDS Security Group 3306 허용&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.1 variables.tf&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;project_name&quot; {
  description = &quot;Project name&quot;
  type        = string
}

variable &quot;vpc_id&quot; {
  description = &quot;VPC ID&quot;
  type        = string
}

variable &quot;private_subnet_ids&quot; {
  description = &quot;Private subnet IDs for RDS&quot;
  type        = list(string)
}

variable &quot;app_security_group_id&quot; {
  description = &quot;Application security group ID&quot;
  type        = string
}

variable &quot;db_name&quot; {
  description = &quot;Database name&quot;
  type        = string
}

variable &quot;db_username&quot; {
  description = &quot;Database master username&quot;
  type        = string
}

variable &quot;db_instance_class&quot; {
  description = &quot;RDS instance class&quot;
  type        = string
  default     = &quot;db.t3.micro&quot;
}

variable &quot;db_allocated_storage&quot; {
  description = &quot;RDS allocated storage&quot;
  type        = number
  default     = 20
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.2 terraform.tfvars&lt;/h3&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;project_name = &quot;demo&quot;

vpc_id = &quot;vpc-xxxxxxxx&quot;

private_subnet_ids = [
  &quot;subnet-aaaaaaaa&quot;,
  &quot;subnet-bbbbbbbb&quot;
]

app_security_group_id = &quot;sg-xxxxxxxx&quot;

db_name     = &quot;appdb&quot;
db_username = &quot;admin&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 프로젝트에서는 &lt;code&gt;vpc_id&lt;/code&gt;, &lt;code&gt;private_subnet_ids&lt;/code&gt;, &lt;code&gt;app_security_group_id&lt;/code&gt;를 직접 쓰기보다 network state output에서 가져올 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.3 locals.tf&lt;/h3&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;locals {
  common_tags = {
    Project   = var.project_name
    ManagedBy = &quot;terraform&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.4 main.tf&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_db_subnet_group&quot; &quot;main&quot; {
  name       = &quot;${var.project_name}-db-subnet-group&quot;
  subnet_ids = var.private_subnet_ids

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-db-subnet-group&quot;
  })
}

resource &quot;aws_security_group&quot; &quot;rds&quot; {
  name        = &quot;${var.project_name}-rds-sg&quot;
  description = &quot;Security group for RDS MySQL&quot;
  vpc_id      = var.vpc_id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-rds-sg&quot;
  })
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;rds_from_app&quot; {
  security_group_id = aws_security_group.rds.id

  referenced_security_group_id = var.app_security_group_id
  ip_protocol                  = &quot;tcp&quot;
  from_port                    = 3306
  to_port                      = 3306

  description = &quot;Allow MySQL access from app security group&quot;
}

resource &quot;aws_vpc_security_group_egress_rule&quot; &quot;rds_all&quot; {
  security_group_id = aws_security_group.rds.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;-1&quot;

  description = &quot;Allow all outbound traffic&quot;
}

resource &quot;aws_db_instance&quot; &quot;mysql&quot; {
  identifier = &quot;${var.project_name}-mysql&quot;

  engine         = &quot;mysql&quot;
  engine_version = &quot;8.0&quot;
  instance_class = var.db_instance_class

  allocated_storage     = var.db_allocated_storage
  max_allocated_storage = 100
  storage_type          = &quot;gp3&quot;
  storage_encrypted     = true

  db_name  = var.db_name
  username = var.db_username

  manage_master_user_password = true

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  publicly_accessible = false

  backup_retention_period = 7
  backup_window           = &quot;18:00-19:00&quot;
  maintenance_window      = &quot;sun:19:00-sun:20:00&quot;

  deletion_protection       = true
  skip_final_snapshot       = false
  final_snapshot_identifier = &quot;${var.project_name}-mysql-final-snapshot&quot;

  auto_minor_version_upgrade = true
  copy_tags_to_snapshot      = true

  lifecycle {
    prevent_destroy = true
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-mysql&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제의 특징은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Private Subnet 배치
RDS Security Group 분리
App Security Group에서만 3306 접근 허용
Public 접근 비활성화
Storage 암호화
자동 백업 7일
삭제 보호 활성화
Final Snapshot 생성
Terraform prevent_destroy 활성화
RDS가 master password를 Secrets Manager로 관리&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.5 outputs.tf&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;output &quot;rds_endpoint&quot; {
  description = &quot;RDS endpoint&quot;
  value       = aws_db_instance.mysql.endpoint
}

output &quot;rds_address&quot; {
  description = &quot;RDS address&quot;
  value       = aws_db_instance.mysql.address
}

output &quot;rds_port&quot; {
  description = &quot;RDS port&quot;
  value       = aws_db_instance.mysql.port
}

output &quot;rds_security_group_id&quot; {
  description = &quot;RDS security group ID&quot;
  value       = aws_security_group.rds.id
}

output &quot;rds_master_user_secret_arn&quot; {
  description = &quot;RDS managed master user secret ARN&quot;
  value       = aws_db_instance.mysql.master_user_secret[0].secret_arn
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;rds_endpoint&lt;/code&gt;는 애플리케이션에서 DB 접속 주소로 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 master password를 Secrets Manager로 관리하는 경우 애플리케이션이 Secret을 읽을 수 있는 IAM 권한도 함께 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. RDS 의존성 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 여러 리소스를 참조한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;VPC
├── Private Subnet
│   └── DB Subnet Group
│       └── RDS
└── Security Group
    └── RDS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 정확히 보면 RDS는 DB Subnet Group과 Security Group을 함께 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Decision Options Flow-2026-05-13-063920.png&quot; data-origin-width=&quot;2603&quot; data-origin-height=&quot;2045&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oTdjZ/dJMcaiXy1Ff/0mCFDor3mu6PTClS8gqa81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oTdjZ/dJMcaiXy1Ff/0mCFDor3mu6PTClS8gqa81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oTdjZ/dJMcaiXy1Ff/0mCFDor3mu6PTClS8gqa81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoTdjZ%2FdJMcaiXy1Ff%2F0mCFDor3mu6PTClS8gqa81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2603&quot; height=&quot;2045&quot; data-filename=&quot;Decision Options Flow-2026-05-13-063920.png&quot; data-origin-width=&quot;2603&quot; data-origin-height=&quot;2045&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 보면 RDS가 단독으로 만들어지는 리소스가 아니라는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크와 보안, 비밀번호 관리가 함께 연결되어야 실제로 사용할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 자주 하는 실수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.1 RDS를 Public으로 열어둠&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS를 인터넷에서 직접 접근 가능하게 만드는 것은 위험하다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;publicly_accessible = true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별한 이유가 없다면 RDS는 Private Subnet에 두고 다음처럼 설정하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;publicly_accessible = false&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.2 RDS Security Group을 0.0.0.0/0으로 열어둠&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 3306 포트를 전체 인터넷에 열면 매우 위험하다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;# 권장하지 않음
0.0.0.0/0 &amp;rarr; 3306&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 App Security Group에서만 접근하도록 제한한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;App Security Group &amp;rarr; RDS Security Group &amp;rarr; 3306&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.3 비밀번호를 terraform.tfvars에 평문으로 저장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB password를 tfvars에 직접 넣는 것은 위험할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;db_password = &quot;my-secret-password&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 RDS의 &lt;code&gt;manage_master_user_password&lt;/code&gt; 기능이나 Secrets Manager를 사용하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.4 sensitive = true면 완전히 안전하다고 생각함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform variable에 &lt;code&gt;sensitive = true&lt;/code&gt;를 붙이면 CLI 출력에서는 숨겨질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 값이 state에 저장될 수 있다는 점은 여전히 주의해야 한다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;sensitive = true
&amp;rarr; 화면 출력 숨김

state 저장 가능성
&amp;rarr; 별도 관리 필요&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.5 skip_final_snapshot = true를 운영에 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습용 RDS는 삭제를 쉽게 하기 위해 final snapshot을 생략할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;skip_final_snapshot = true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 운영 DB에서는 삭제 전 최종 스냅샷을 남기는 것이 안전하다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;skip_final_snapshot = false&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.6 deletion_protection을 끄고 운영함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 DB는 실수로 삭제되면 치명적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하면 삭제 보호를 켜는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;deletion_protection = true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Terraform의 &lt;code&gt;prevent_destroy&lt;/code&gt;도 함께 고려할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.7 backup_retention_period를 0으로 둠&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;backup_retention_period = 0&lt;/code&gt;은 자동 백업을 비활성화하는 의미로 사용될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 자동 백업을 반드시 고려해야 한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;backup_retention_period = 7&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.8 RDS 생성 직후 바로 애플리케이션을 실행함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 Terraform apply가 끝났다고 해서 애플리케이션이 즉시 안정적으로 접속 가능한 상태라고 단정하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS 상태가 &lt;code&gt;available&lt;/code&gt;인지 확인하고, 네트워크와 Secret 권한까지 확인한 뒤 애플리케이션을 실행하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;RDS 생성
&amp;rarr; available 상태 확인
&amp;rarr; Secret 접근 확인
&amp;rarr; App 실행&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.9 RDS 비용을 잊음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 EC2처럼 실행 중이면 비용이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 스토리지, 백업, 스냅샷에도 비용이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습용으로 생성했다면 실습 후 반드시 정리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 삭제 보호나 prevent_destroy를 켜두었다면 먼저 해당 설정을 해제해야 destroy할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Terraform으로 RDS MySQL을 구현하는 방법을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 단순히 DB 인스턴스 하나를 만드는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 요소를 함께 고려해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Private Subnet
DB Subnet Group
Security Group
Backup
Snapshot
Deletion Protection
Password Management
Secrets Manager
Terraform prevent_destroy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 RDS는 삭제되면 큰 문제가 생길 수 있는 리소스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 운영 환경에서는 삭제 보호와 백업, final snapshot을 반드시 고려하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 DB password를 Terraform 코드나 tfvars에 직접 넣기보다, Secrets Manager를 활용하는 구조가 더 안전하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;한 줄 정리&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDS는 생성보다 네트워크 제한, 백업, 삭제 보호, 비밀번호 관리가 더 중요하다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 Secrets Manager와 SSM Parameter Store를 Terraform으로 구현해본다. 민감정보와 운영 설정값을 어떻게 분리해서 관리할지 정리할 예정이다.&lt;/p&gt;</description>
      <category>테라폼</category>
      <author>pininini</author>
      <guid isPermaLink="true">https://pininininfra.tistory.com/19</guid>
      <comments>https://pininininfra.tistory.com/19#entry19comment</comments>
      <pubDate>Wed, 13 May 2026 15:43:48 +0900</pubDate>
    </item>
    <item>
      <title>4-6. 테라폼 - S3 구현하기</title>
      <link>https://pininininfra.tistory.com/18</link>
      <description>&lt;h1 style=&quot;text-align: center;&quot;&gt;테라폼 - S3 구현하기&lt;/h1&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Bucket, Public Access Block, Versioning, Encryption, Lifecycle까지 안전하게 구성하기&lt;/i&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 Terraform으로 EC2를 구현하는 방법을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 AWS에서 가장 자주 사용하는 저장소 서비스인 &lt;b&gt;S3&lt;/b&gt;를 Terraform으로 구현해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3는 처음 보면 단순한 파일 저장소처럼 보인다. 하지만 실제로 운영 환경에서 사용하려면 단순히 버킷 하나를 만드는 것으로 끝나지 않는다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;S3 Bucket
Public Access Block
Versioning
Server-Side Encryption
Lifecycle Rule
Bucket Policy
force_destroy
prevent_destroy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 S3는 실수로 공개되거나, 삭제되면 안 되는 데이터를 삭제하거나, 오래된 객체가 계속 쌓여 비용이 증가하는 문제가 생기기 쉽다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;S3는 만들기보다 안전하게 공개하지 않고, 안전하게 보존하고, 비용이 쌓이지 않게 관리하는 것이 더 중요하다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. S3란 무엇인가&lt;/li&gt;
&lt;li&gt;2. S3를 만들 때 함께 고려할 것들&lt;/li&gt;
&lt;li&gt;3. 기본 S3 Bucket 생성&lt;/li&gt;
&lt;li&gt;4. Public Access Block 설정&lt;/li&gt;
&lt;li&gt;5. Versioning 설정&lt;/li&gt;
&lt;li&gt;6. Server-Side Encryption 설정&lt;/li&gt;
&lt;li&gt;7. Lifecycle Rule 설정&lt;/li&gt;
&lt;li&gt;8. Bucket Policy 설정&lt;/li&gt;
&lt;li&gt;9. force_destroy와 prevent_destroy&lt;/li&gt;
&lt;li&gt;10. 실전 예제: 일반 애플리케이션 파일 저장용 S3&lt;/li&gt;
&lt;li&gt;11. 특수 케이스 1: Terraform Backend용 S3&lt;/li&gt;
&lt;li&gt;12. 특수 케이스 2: CloudFront Origin용 S3&lt;/li&gt;
&lt;li&gt;13. S3 의존성 흐름&lt;/li&gt;
&lt;li&gt;14. 자주 하는 실수&lt;/li&gt;
&lt;li&gt;15. 마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. S3란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3는 Simple Storage Service의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 AWS에서 제공하는 객체 스토리지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 디렉토리 구조처럼 저장한다고 생각하기 쉽지만, 정확히는 객체를 버킷 안에 저장하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;S3 Bucket
└── Object
    ├── key
    ├── data
    └── metadata&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3는 다음과 같은 용도로 많이 사용된다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;이미지 저장
첨부파일 저장
로그 저장
백업 파일 저장
정적 웹사이트 파일 저장
Terraform state 저장
CloudFront Origin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3는 매우 범용적인 저장소이기 때문에 사용 범위가 넓다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그만큼 보안 설정도 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. S3를 만들 때 함께 고려할 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform으로 S3를 구성할 때는 단순히 &lt;code&gt;aws_s3_bucket&lt;/code&gt;만 만들지 않는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 다음 리소스들을 함께 고려한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;리소스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_s3_bucket&lt;/td&gt;
&lt;td&gt;S3 Bucket 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_s3_bucket_public_access_block&lt;/td&gt;
&lt;td&gt;버킷 공개 접근 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_s3_bucket_versioning&lt;/td&gt;
&lt;td&gt;객체 버전 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_s3_bucket_server_side_encryption_configuration&lt;/td&gt;
&lt;td&gt;서버 측 암호화 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_s3_bucket_lifecycle_configuration&lt;/td&gt;
&lt;td&gt;오래된 객체 또는 버전 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_s3_bucket_policy&lt;/td&gt;
&lt;td&gt;버킷 접근 정책&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_s3_bucket_ownership_controls&lt;/td&gt;
&lt;td&gt;객체 소유권 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자라면 다음 기준으로 시작하면 좋다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;기본은 private bucket
Public Access Block 활성화
Versioning 활성화
Encryption 명시
Lifecycle로 오래된 버전 정리
Bucket Policy는 필요한 경우에만 작성&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 기본 S3 Bucket 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 S3 Bucket은 다음처럼 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;resource &quot;aws_s3_bucket&quot; &quot;app&quot; {
  bucket = &quot;${var.project_name}-app-bucket&quot;

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-app-bucket&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;bucket&lt;/code&gt;은 버킷 이름이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 버킷 이름은 전 세계에서 유일해야 한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;bucket = &quot;${var.project_name}-app-bucket&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위 코드만으로는 운영 환경에 사용하기 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3는 보안과 데이터 보호 설정을 함께 구성해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Public Access Block 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3에서 가장 먼저 신경 써야 하는 것은 공개 접근 차단이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 버킷이 실수로 공개되면 민감한 파일이 외부에 노출될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 일반적인 애플리케이션 파일 저장소는 Public Access Block을 켜는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_s3_bucket_public_access_block&quot; &quot;app&quot; {
  bucket = aws_s3_bucket.app.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정의 의미는 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;설정&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;block_public_acls&lt;/td&gt;
&lt;td&gt;공개 ACL 설정 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ignore_public_acls&lt;/td&gt;
&lt;td&gt;기존 공개 ACL 무시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;block_public_policy&lt;/td&gt;
&lt;td&gt;공개 Bucket Policy 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;restrict_public_buckets&lt;/td&gt;
&lt;td&gt;공개 정책이 있어도 접근 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 private bucket에서는 네 값을 모두 &lt;code&gt;true&lt;/code&gt;로 두는 것이 안전하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;S3는 기본적으로 공개하지 않는 방향으로 설계하는 것이 좋다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Versioning 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Versioning은 S3 객체의 여러 버전을 보관하는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 같은 key에 파일을 다시 업로드하면 기존 객체를 덮어쓰는 것이 아니라, 이전 버전을 함께 보관할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;file.txt v1
file.txt v2
file.txt v3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;resource &quot;aws_s3_bucket_versioning&quot; &quot;app&quot; {
  bucket = aws_s3_bucket.app.id

  versioning_configuration {
    status = &quot;Enabled&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Versioning을 켜면 실수로 파일을 덮어썼을 때 복구할 가능성이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 버전이 계속 쌓이면 비용도 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Versioning을 켠다면 Lifecycle Rule도 함께 고려하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Versioning
&amp;rarr; 데이터 보호

Lifecycle
&amp;rarr; 오래된 버전 비용 관리&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Server-Side Encryption 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3에 저장되는 객체는 암호화해서 보관하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에서 기본 암호화를 제공하더라도, Terraform 코드에서 암호화 설정을 명시해두면 의도를 분명하게 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 SSE-S3 암호화 설정은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;resource &quot;aws_s3_bucket_server_side_encryption_configuration&quot; &quot;app&quot; {
  bucket = aws_s3_bucket.app.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = &quot;AES256&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 Amazon S3 관리형 키를 사용한 서버 측 암호화다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;SSE-S3
&amp;rarr; sse_algorithm = &quot;AES256&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KMS 키를 사용하고 싶다면 다음처럼 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_kms_key&quot; &quot;s3&quot; {
  description             = &quot;KMS key for S3 bucket encryption&quot;
  deletion_window_in_days = 7

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-s3-kms-key&quot;
  })
}

resource &quot;aws_s3_bucket_server_side_encryption_configuration&quot; &quot;app&quot; {
  bucket = aws_s3_bucket.app.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = &quot;aws:kms&quot;
      kms_master_key_id = aws_kms_key.s3.arn
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 KMS를 사용하면 권한 관리가 추가로 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보 단계에서는 먼저 SSE-S3로 시작하고, KMS가 필요한 경우 별도로 확장하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Lifecycle Rule 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3는 객체가 계속 쌓이면 비용이 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 Versioning을 켠 경우 오래된 버전이 계속 남을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lifecycle Rule을 사용하면 오래된 객체나 오래된 버전을 자동으로 정리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_s3_bucket_lifecycle_configuration&quot; &quot;app&quot; {
  bucket = aws_s3_bucket.app.id

  rule {
    id     = &quot;delete-old-noncurrent-versions&quot;
    status = &quot;Enabled&quot;

    filter {
      prefix = &quot;&quot;
    }

    noncurrent_version_expiration {
      noncurrent_days = 30
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 설정은 현재 버전이 아닌 오래된 버전을 30일 후 삭제한다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;noncurrent version
&amp;rarr; 현재 버전이 아닌 이전 버전&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 저장용 버킷이라면 일정 기간이 지난 객체를 삭제할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_s3_bucket_lifecycle_configuration&quot; &quot;logs&quot; {
  bucket = aws_s3_bucket.logs.id

  rule {
    id     = &quot;delete-old-logs&quot;
    status = &quot;Enabled&quot;

    filter {
      prefix = &quot;&quot;
    }

    expiration {
      days = 90
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 90일이 지난 객체를 삭제한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Versioning은 데이터를 보호하고, Lifecycle은 비용을 관리한다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Bucket Policy 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bucket Policy는 S3 Bucket에 대한 접근 정책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 특정 IAM Role만 버킷의 객체를 읽을 수 있도록 허용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;data &quot;aws_iam_policy_document&quot; &quot;app_bucket_read&quot; {
  statement {
    effect = &quot;Allow&quot;

    principals {
      type        = &quot;AWS&quot;
      identifiers = [aws_iam_role.app.arn]
    }

    actions = [
      &quot;s3:GetObject&quot;
    ]

    resources = [
      &quot;${aws_s3_bucket.app.arn}/*&quot;
    ]
  }
}

resource &quot;aws_s3_bucket_policy&quot; &quot;app&quot; {
  bucket = aws_s3_bucket.app.id
  policy = data.aws_iam_policy_document.app_bucket_read.json
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 일반적인 private bucket에서는 반드시 Bucket Policy가 필요한 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2, ECS, Lambda 같은 리소스가 S3에 접근해야 한다면 보통 해당 리소스의 IAM Role에 S3 접근 권한을 부여하는 방식으로도 충분하다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;EC2 Role
&amp;rarr; s3:GetObject 허용

S3 Bucket
&amp;rarr; Public Access Block 유지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bucket Policy는 다음 상황에서 자주 사용한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;CloudFront만 S3에 접근 허용
특정 AWS 계정만 접근 허용
특정 VPC Endpoint를 통한 접근만 허용
특정 조건의 요청만 허용
강제 암호화 업로드 정책&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Bucket Policy는 버킷 자체의 접근 경계를 만들 때 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. force_destroy와 prevent_destroy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3는 삭제할 때 자주 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버킷 안에 객체가 남아 있으면 기본적으로 버킷 삭제가 실패할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;BucketNotEmpty&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서는 &lt;code&gt;force_destroy&lt;/code&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_s3_bucket&quot; &quot;app&quot; {
  bucket = &quot;${var.project_name}-app-bucket&quot;

  force_destroy = true

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-app-bucket&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;force_destroy = true&lt;/code&gt;를 사용하면 버킷 삭제 시 내부 객체까지 함께 삭제될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습용 버킷에는 편리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 운영 데이터가 들어 있는 버킷에는 매우 위험할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;학습용:
force_destroy = true 가능

운영 데이터:
force_destroy = false 권장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 버킷은 &lt;code&gt;prevent_destroy&lt;/code&gt;로 보호할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;resource &quot;aws_s3_bucket&quot; &quot;app&quot; {
  bucket = &quot;${var.project_name}-app-bucket&quot;

  lifecycle {
    prevent_destroy = true
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-app-bucket&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 Terraform이 해당 버킷을 삭제하려고 할 때 오류를 발생시킨다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;운영 데이터 버킷은 삭제 편의성보다 삭제 방지가 더 중요하다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 실전 예제: 일반 애플리케이션 파일 저장용 S3&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 일반 애플리케이션에서 첨부파일이나 이미지를 저장하는 private S3 Bucket 예제를 정리해보자.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.1 variables.tf&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;project_name&quot; {
  description = &quot;Project name&quot;
  type        = string
}

variable &quot;bucket_name&quot; {
  description = &quot;S3 bucket name&quot;
  type        = string
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.2 terraform.tfvars&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;project_name = &quot;demo&quot;
bucket_name  = &quot;demo-app-files-123456&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 버킷 이름은 전 세계에서 유일해야 하므로 프로젝트 이름만으로는 중복될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 계정 ID, 환경명, 랜덤 suffix 등을 함께 사용하는 경우가 많다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.3 locals.tf&lt;/h3&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;locals {
  common_tags = {
    Project   = var.project_name
    ManagedBy = &quot;terraform&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.4 main.tf&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_s3_bucket&quot; &quot;app&quot; {
  bucket = var.bucket_name

  force_destroy = false

  tags = merge(local.common_tags, {
    Name = var.bucket_name
  })
}

resource &quot;aws_s3_bucket_public_access_block&quot; &quot;app&quot; {
  bucket = aws_s3_bucket.app.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource &quot;aws_s3_bucket_versioning&quot; &quot;app&quot; {
  bucket = aws_s3_bucket.app.id

  versioning_configuration {
    status = &quot;Enabled&quot;
  }
}

resource &quot;aws_s3_bucket_server_side_encryption_configuration&quot; &quot;app&quot; {
  bucket = aws_s3_bucket.app.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = &quot;AES256&quot;
    }
  }
}

resource &quot;aws_s3_bucket_lifecycle_configuration&quot; &quot;app&quot; {
  bucket = aws_s3_bucket.app.id

  rule {
    id     = &quot;delete-old-noncurrent-versions&quot;
    status = &quot;Enabled&quot;

    filter {
      prefix = &quot;&quot;
    }

    noncurrent_version_expiration {
      noncurrent_days = 30
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구성은 다음 성격을 가진다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Private Bucket
Public Access Block 활성화
Versioning 활성화
SSE-S3 암호화 명시
오래된 이전 버전 30일 후 삭제
force_destroy 비활성화&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.5 outputs.tf&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;output &quot;s3_bucket_id&quot; {
  description = &quot;S3 bucket ID&quot;
  value       = aws_s3_bucket.app.id
}

output &quot;s3_bucket_arn&quot; {
  description = &quot;S3 bucket ARN&quot;
  value       = aws_s3_bucket.app.arn
}

output &quot;s3_bucket_name&quot; {
  description = &quot;S3 bucket name&quot;
  value       = aws_s3_bucket.app.bucket
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 output 값은 이후 IAM Policy나 애플리케이션 설정에서 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;IAM Policy
&amp;rarr; S3 Bucket ARN 참조

Application
&amp;rarr; S3 Bucket Name 사용&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 특수 케이스 1: Terraform Backend용 S3&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3는 Terraform state를 저장하는 backend로도 많이 사용된다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;Terraform
&amp;rarr; S3 Bucket에 state 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backend용 S3는 일반 애플리케이션 파일 저장소보다 더 조심해서 다뤄야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 다음 설정을 고려한다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;Public Access Block
Versioning
Encryption
prevent_destroy
접근 권한 제한&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backend용 S3 예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_s3_bucket&quot; &quot;tfstate&quot; {
  bucket = &quot;${var.project_name}-tfstate&quot;

  lifecycle {
    prevent_destroy = true
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-tfstate&quot;
  })
}

resource &quot;aws_s3_bucket_public_access_block&quot; &quot;tfstate&quot; {
  bucket = aws_s3_bucket.tfstate.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource &quot;aws_s3_bucket_versioning&quot; &quot;tfstate&quot; {
  bucket = aws_s3_bucket.tfstate.id

  versioning_configuration {
    status = &quot;Enabled&quot;
  }
}

resource &quot;aws_s3_bucket_server_side_encryption_configuration&quot; &quot;tfstate&quot; {
  bucket = aws_s3_bucket.tfstate.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = &quot;AES256&quot;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform backend 설정은 별도 파일에 다음처럼 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;terraform {
  backend &quot;s3&quot; {
    bucket = &quot;demo-tfstate&quot;
    key    = &quot;dev/terraform.tfstate&quot;
    region = &quot;ap-northeast-2&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은 backend용 S3 Bucket은 Terraform이 state를 저장하기 전에 먼저 존재해야 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 backend bucket은 별도 bootstrap 과정으로 만들거나, 처음에는 local state로 만든 뒤 backend로 전환하는 방식을 사용할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 특수 케이스 2: CloudFront Origin용 S3&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 파일을 S3에 두고 CloudFront로 배포하는 구조도 많이 사용된다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;User
&amp;rarr; CloudFront
&amp;rarr; S3 Origin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 S3를 직접 public으로 여는 방식은 권장하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 S3는 private으로 두고, CloudFront만 S3에 접근할 수 있도록 구성하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;권장 구조:
S3 Public Access Block 활성화
CloudFront Origin Access Control 사용
Bucket Policy로 CloudFront만 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구성은 CloudFront 글에서 더 자세히 다루는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 핵심만 기억하면 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;S3 정적 파일을 외부에 제공할 때도 S3를 직접 공개하기보다 CloudFront를 앞에 두는 구성이 더 안전하다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. S3 의존성 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 Bucket과 관련 설정의 의존성 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;S3 Bucket
├── Public Access Block
├── Versioning
├── Encryption
├── Lifecycle Configuration
└── Bucket Policy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Decision Options Flow-2026-05-13-055934.png&quot; data-origin-width=&quot;4173&quot; data-origin-height=&quot;885&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxkiol/dJMcabK0mZH/CYqrce8hmMABFm0k1urYZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxkiol/dJMcabK0mZH/CYqrce8hmMABFm0k1urYZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxkiol/dJMcabK0mZH/CYqrce8hmMABFm0k1urYZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdxkiol%2FdJMcabK0mZH%2FCYqrce8hmMABFm0k1urYZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4173&quot; height=&quot;885&quot; data-filename=&quot;Decision Options Flow-2026-05-13-055934.png&quot; data-origin-width=&quot;4173&quot; data-origin-height=&quot;885&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 Bucket 자체는 독립적으로 만들 수 있지만, 보안과 운영을 위해 여러 설정 리소스를 함께 연결하는 것이 일반적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 분류 기준으로 보면 S3 Bucket은 Independent에 가깝고, Public Access Block, Versioning, Encryption, Lifecycle, Policy는 S3 Bucket을 참조하는 Dependent 리소스라고 볼 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;14. 자주 하는 실수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.1 버킷을 public으로 열어둠&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 위험한 실수 중 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별한 이유가 없다면 S3 Bucket은 private으로 시작하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;Public Access Block
&amp;rarr; 기본 활성화 권장&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.2 Bucket Policy로 public 허용 후 Public Access Block 때문에 안 된다고 헷갈림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 Public Access Block이 켜져 있으면 public Bucket Policy를 작성해도 실제 공개 접근이 차단될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 웹사이트 호스팅처럼 public 접근이 필요한 경우에는 Public Access Block 설정과 Bucket Policy를 함께 이해해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 일반적인 운영에서는 public 접근을 열기보다 CloudFront를 앞에 두는 것이 더 안전하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.3 Versioning만 켜고 Lifecycle을 설정하지 않음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Versioning은 데이터 보호에 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 오래된 버전이 계속 쌓이면 비용이 증가한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;Versioning 활성화
&amp;rarr; 이전 버전 계속 보관
&amp;rarr; 비용 증가 가능
&amp;rarr; Lifecycle 필요&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.4 force_destroy를 운영 버킷에 사용함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;force_destroy = true&lt;/code&gt;는 학습용이나 임시 버킷에는 편리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 운영 데이터가 들어 있는 버킷에 사용하면 위험하다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;terraform destroy
&amp;rarr; 버킷 내부 객체까지 삭제 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요 데이터 버킷에는 &lt;code&gt;force_destroy = false&lt;/code&gt;와 &lt;code&gt;prevent_destroy&lt;/code&gt;를 고려하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.5 Secret이나 Access Key를 S3에 평문 저장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3에 민감정보를 저장해야 한다면 암호화와 접근 권한을 매우 엄격하게 관리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 애플리케이션 Secret은 S3보다 Secrets Manager나 SSM Parameter Store를 사용하는 것이 더 적합하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.6 Bucket Name을 대충 정함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 Bucket 이름은 전 세계에서 유일해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 단순히 &lt;code&gt;app-bucket&lt;/code&gt; 같은 이름은 이미 사용 중일 가능성이 높다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;권장 예:
project-env-purpose-account-id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;demo-dev-app-files-123456789012&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.7 Terraform Backend Bucket을 일반 리소스와 함께 삭제함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform state를 저장하는 S3 Bucket은 매우 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backend용 S3를 일반 app 리소스와 같은 생명주기로 관리하면 위험하다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;app destroy
&amp;rarr; tfstate bucket 삭제 시도
&amp;rarr; state 관리 문제 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backend용 S3는 별도 bootstrap 계층으로 분리하고, 삭제 방지 설정을 두는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;15. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Terraform으로 S3를 구현하는 방법을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3는 단순한 파일 저장소처럼 보이지만, 실제 운영에서는 다음 설정을 함께 고려해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Public Access Block
Versioning
Encryption
Lifecycle
Bucket Policy
force_destroy
prevent_destroy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자라면 먼저 private bucket을 안전하게 만드는 구조부터 익히는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 필요에 따라 Terraform backend용 S3, CloudFront Origin용 S3, 로그 저장용 S3 등으로 확장하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;한 줄 정리&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;S3는 버킷을 만드는 것보다 공개 차단, 보존, 암호화, 비용 관리를 함께 설계하는 것이 중요하다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 RDS MySQL을 Terraform으로 구현해본다. RDS는 DB Subnet Group, Security Group, 백업, 삭제 보호, 비밀번호 관리까지 함께 고려해야 하는 리소스다.&lt;/p&gt;</description>
      <category>테라폼</category>
      <author>pininini</author>
      <guid isPermaLink="true">https://pininininfra.tistory.com/18</guid>
      <comments>https://pininininfra.tistory.com/18#entry18comment</comments>
      <pubDate>Wed, 13 May 2026 15:02:34 +0900</pubDate>
    </item>
    <item>
      <title>4-5. 테라폼 - EC2 구현하기</title>
      <link>https://pininininfra.tistory.com/17</link>
      <description>&lt;h1 style=&quot;text-align: center;&quot;&gt;테라폼 - EC2 구현하기&lt;/h1&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Subnet, Security Group, IAM Role, User Data까지 연결해서 EC2 이해하기&lt;/i&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 IAM Role과 Policy를 Terraform으로 구현하는 방법을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 AWS에서 가장 기본적인 컴퓨팅 리소스인 &lt;b&gt;EC2&lt;/b&gt;를 Terraform으로 구현해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 처음 보면 단순히 &amp;ldquo;서버 한 대&amp;rdquo;처럼 보인다. 하지만 실제로 Terraform으로 EC2를 만들다 보면 여러 리소스가 함께 연결된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;VPC
Subnet
Security Group
Key Pair
IAM Instance Profile
AMI
EBS
User Data
Public IP / Private IP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, EC2는 단독으로 존재하는 리소스가 아니라 네트워크, 보안, 권한, 스토리지와 함께 구성되는 리소스다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;EC2는 서버처럼 보이지만, Terraform에서는 여러 리소스와 연결된 교체 가능한 인프라 리소스다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. EC2란 무엇인가&lt;/li&gt;
&lt;li&gt;2. EC2를 만들 때 필요한 요소&lt;/li&gt;
&lt;li&gt;3. Public EC2와 Private EC2&lt;/li&gt;
&lt;li&gt;4. AMI 조회하기&lt;/li&gt;
&lt;li&gt;5. Key Pair 설정하기&lt;/li&gt;
&lt;li&gt;6. 기본 EC2 생성 코드&lt;/li&gt;
&lt;li&gt;7. IAM Instance Profile 연결하기&lt;/li&gt;
&lt;li&gt;8. User Data로 초기 설정하기&lt;/li&gt;
&lt;li&gt;9. EBS 설정하기&lt;/li&gt;
&lt;li&gt;10. Elastic IP 연결하기&lt;/li&gt;
&lt;li&gt;11. EC2 replacement와 private IP 변경 문제&lt;/li&gt;
&lt;li&gt;12. EC2 의존성 흐름&lt;/li&gt;
&lt;li&gt;13. 자주 하는 실수&lt;/li&gt;
&lt;li&gt;14. 마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. EC2란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 Elastic Compute Cloud의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 AWS에서 제공하는 가상 서버다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2를 사용하면 직접 물리 서버를 구매하지 않고도 필요한 사양의 서버를 빠르게 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;EC2
&amp;rarr; AWS에서 생성하는 가상 서버&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 다음과 같은 용도로 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;웹 서버
API 서버
배치 서버
관리자용 서버
Bastion 서버
테스트용 서버
모니터링 서버&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 EC2 하나만 만든다고 서비스가 완성되는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2가 정상적으로 동작하려면 네트워크, 보안, 권한, 스토리지 설정이 함께 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. EC2를 만들 때 필요한 요소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2를 Terraform으로 만들 때 기본적으로 다음 요소를 고려해야 한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;요소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AMI&lt;/td&gt;
&lt;td&gt;EC2에 설치될 OS 이미지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instance Type&lt;/td&gt;
&lt;td&gt;CPU, 메모리 등 서버 사양&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subnet&lt;/td&gt;
&lt;td&gt;EC2가 배치될 네트워크 구역&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Group&lt;/td&gt;
&lt;td&gt;EC2의 inbound / outbound 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key Pair&lt;/td&gt;
&lt;td&gt;SSH 접속에 사용할 공개키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM Instance Profile&lt;/td&gt;
&lt;td&gt;EC2가 AWS API를 호출할 때 사용할 권한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Data&lt;/td&gt;
&lt;td&gt;EC2 최초 부팅 시 실행할 스크립트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EBS&lt;/td&gt;
&lt;td&gt;EC2에 연결되는 디스크&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 코드에서는 보통 다음처럼 연결된다.&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;aws_instance
├── ami
├── instance_type
├── subnet_id
├── vpc_security_group_ids
├── key_name
├── iam_instance_profile
├── user_data
└── root_block_device&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Public EC2와 Private EC2&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 어떤 Subnet에 배치하느냐에 따라 Public EC2와 Private EC2로 나눌 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;사용 예&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public EC2&lt;/td&gt;
&lt;td&gt;Public Subnet에 위치하고 Public IP를 가질 수 있음&lt;/td&gt;
&lt;td&gt;간단한 웹 서버, Bastion 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private EC2&lt;/td&gt;
&lt;td&gt;Private Subnet에 위치하고 외부에서 직접 접근하지 않음&lt;/td&gt;
&lt;td&gt;내부 API 서버, 배치 서버, 관리 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Public EC2가 인터넷과 통신하려면 다음 조건이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. Public Subnet에 위치
2. Public IP 또는 Elastic IP 보유
3. Route Table에 0.0.0.0/0 &amp;rarr; Internet Gateway 경로 존재
4. Security Group / NACL 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Private EC2는 보통 Public IP가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Private EC2가 외부 인터넷으로 나가야 한다면 NAT Gateway나 VPC Endpoint 같은 별도 경로를 고려해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Private EC2
&amp;rarr; NAT Gateway
&amp;rarr; Internet Gateway
&amp;rarr; Internet&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자 실습에서는 Public EC2를 먼저 만들어보는 것이 이해하기 쉽다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. AMI 조회하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2를 만들려면 AMI가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AMI는 Amazon Machine Image의 약자로, EC2에 설치할 OS 이미지라고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Amazon Linux 2 최신 AMI를 조회하려면 다음처럼 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;data &quot;aws_ami&quot; &quot;amazon_linux_2&quot; {
  most_recent = true
  owners      = [&quot;amazon&quot;]

  filter {
    name   = &quot;name&quot;
    values = [&quot;amzn2-ami-hvm-*-x86_64-gp2&quot;]
  }

  filter {
    name   = &quot;virtualization-type&quot;
    values = [&quot;hvm&quot;]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;data&lt;/code&gt;는 Terraform이 직접 생성하지 않고, AWS에 이미 존재하는 값을 조회할 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에서는 다음처럼 사용한다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;ami = data.aws_ami.amazon_linux_2.id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AMI ID를 직접 하드코딩할 수도 있지만, 리전마다 AMI ID가 다르고 시간이 지나면 바뀔 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 학습이나 간단한 템플릿에서는 &lt;code&gt;data &quot;aws_ami&quot;&lt;/code&gt;를 사용해 조회하는 방식이 편하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Key Pair 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에 SSH로 접속하려면 Key Pair를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서는 공개키를 AWS에 등록하고, EC2에 해당 Key Pair 이름을 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 로컬에서 키를 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;ssh-keygen -t ed25519 -f ~/.ssh/demo-ec2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 다음 파일이 생성된다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;~/.ssh/demo-ec2      &amp;rarr; private key
~/.ssh/demo-ec2.pub  &amp;rarr; public key&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에는 public key만 등록한다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_key_pair&quot; &quot;ec2&quot; {
  key_name   = &quot;${var.project_name}-ec2-key&quot;
  public_key = file(var.public_key_path)

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2-key&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수는 다음처럼 정의할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;public_key_path&quot; {
  description = &quot;Path to public key file&quot;
  type        = string
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;terraform.tfvars&lt;/code&gt;에는 다음처럼 입력한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;public_key_path = &quot;~/.ssh/demo-ec2.pub&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은 private key를 Git에 올리면 안 된다는 것이다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# 절대 Git에 올리면 안 됨
~/.ssh/demo-ec2&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Terraform에는 public key만 등록하고, private key는 로컬에서 안전하게 관리해야 한다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 기본 EC2 생성 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 가장 기본적인 EC2를 만들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제는 Public Subnet에 EC2를 생성하고, Security Group과 Key Pair를 연결한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 variables.tf&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;project_name&quot; {
  description = &quot;Project name&quot;
  type        = string
}

variable &quot;instance_type&quot; {
  description = &quot;EC2 instance type&quot;
  type        = string
  default     = &quot;t3.micro&quot;
}

variable &quot;public_key_path&quot; {
  description = &quot;Path to public key file&quot;
  type        = string
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 terraform.tfvars&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;project_name    = &quot;demo&quot;
instance_type   = &quot;t3.micro&quot;
public_key_path = &quot;~/.ssh/demo-ec2.pub&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 EC2 코드&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;app&quot; {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.ec2.id]
  key_name               = aws_key_pair.ec2.key_name

  associate_public_ip_address = true

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정의 의미는 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ami&lt;/td&gt;
&lt;td&gt;EC2에 사용할 OS 이미지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;instance_type&lt;/td&gt;
&lt;td&gt;EC2 사양&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;subnet_id&lt;/td&gt;
&lt;td&gt;EC2가 배치될 Subnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vpc_security_group_ids&lt;/td&gt;
&lt;td&gt;EC2에 연결할 Security Group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key_name&lt;/td&gt;
&lt;td&gt;SSH 접속에 사용할 Key Pair&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;associate_public_ip_address&lt;/td&gt;
&lt;td&gt;Public IP 자동 할당 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.4 SSH 접속&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2가 생성되면 output으로 public IP를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;terraform output -raw ec2_public_ip&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 접속은 다음처럼 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;ssh -i ~/.ssh/demo-ec2 ec2-user@EC2_PUBLIC_IP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Amazon Linux 계열은 기본 사용자가 보통 &lt;code&gt;ec2-user&lt;/code&gt;다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. IAM Instance Profile 연결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 내부 애플리케이션이 AWS API를 호출해야 하는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 작업이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;S3 객체 읽기
Secrets Manager 값 읽기
SSM Parameter 조회
CloudWatch Logs 전송&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Access Key를 EC2 안에 넣는 것은 권장하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 IAM Role을 만들고, Instance Profile을 통해 EC2에 연결한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EC2
&amp;rarr; Instance Profile
&amp;rarr; IAM Role
&amp;rarr; Permission Policy&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 IAM Role&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;ec2&quot; {
  name = &quot;${var.project_name}-ec2-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ec2.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2-role&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 Instance Profile&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;resource &quot;aws_iam_instance_profile&quot; &quot;ec2&quot; {
  name = &quot;${var.project_name}-ec2-profile&quot;
  role = aws_iam_role.ec2.name
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 EC2에 연결&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;app&quot; {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.ec2.id]
  key_name               = aws_key_pair.ec2.key_name

  iam_instance_profile = aws_iam_instance_profile.ec2.name

  associate_public_ip_address = true

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 EC2 내부에서는 해당 Role 권한으로 AWS API를 호출할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;EC2에서 AWS API를 호출해야 한다면 Access Key를 넣지 말고 IAM Role을 붙이는 것이 좋다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. User Data로 초기 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User Data는 EC2가 처음 생성될 때 실행되는 초기화 스크립트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 EC2 생성 시 Apache 웹 서버를 설치하고 간단한 페이지를 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;app&quot; {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.ec2.id]
  key_name               = aws_key_pair.ec2.key_name

  associate_public_ip_address = true

  user_data = &amp;lt;&amp;lt;-EOF
              #!/bin/bash
              yum update -y
              yum install -y httpd
              systemctl enable httpd
              systemctl start httpd
              echo &quot;Hello Terraform EC2&quot; &amp;gt; /var/www/html/index.html
              EOF

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 브라우저에서 다음 주소로 접속하면 된다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://EC2_PUBLIC_IP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, Security Group에서 80번 포트가 열려 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User Data에 대해 주의할 점이 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;User Data는 일반적으로 최초 부팅 시 실행된다.
기존 EC2의 user_data를 바꿨다고 해서 항상 스크립트가 다시 실행되는 것은 아니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 애플리케이션 배포나 반복 실행이 필요한 설정은 User Data만으로 처리하기보다, CI/CD, Ansible, SSM Run Command, 이미지 빌드 방식 등을 고려할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서 User Data 변경 시 EC2 교체를 의도적으로 유도하고 싶다면 &lt;code&gt;user_data_replace_on_change&lt;/code&gt; 같은 옵션을 고려할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;user_data_replace_on_change = true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 경우 User Data 변경이 EC2 replacement로 이어질 수 있으므로 운영 환경에서는 주의해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. EBS 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에는 기본적으로 root volume이 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서는 &lt;code&gt;root_block_device&lt;/code&gt;로 root volume 설정을 조정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;app&quot; {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.ec2.id]

  root_block_device {
    volume_size           = 20
    volume_type           = &quot;gp3&quot;
    delete_on_termination = true
    encrypted             = true
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 설정은 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;설정&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;volume_size&lt;/td&gt;
&lt;td&gt;디스크 크기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;volume_type&lt;/td&gt;
&lt;td&gt;EBS 타입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delete_on_termination&lt;/td&gt;
&lt;td&gt;EC2 삭제 시 root volume도 삭제할지 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;encrypted&lt;/td&gt;
&lt;td&gt;EBS 암호화 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 EBS Volume을 별도로 만들고 EC2에 붙일 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_ebs_volume&quot; &quot;data&quot; {
  availability_zone = aws_instance.app.availability_zone
  size              = 20
  type              = &quot;gp3&quot;
  encrypted         = true

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-data-volume&quot;
  })
}

resource &quot;aws_volume_attachment&quot; &quot;data&quot; {
  device_name = &quot;/dev/sdf&quot;
  volume_id   = aws_ebs_volume.data.id
  instance_id = aws_instance.app.id
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 EBS Volume은 EC2와 별도 생명주기를 가질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 EC2 교체나 삭제 시 EBS를 어떻게 유지할지 명확히 설계해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. Elastic IP 연결하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에 자동 할당되는 Public IP는 EC2가 중지되거나 교체되면 바뀔 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고정 Public IP가 필요하다면 Elastic IP를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_eip&quot; &quot;app&quot; {
  domain = &quot;vpc&quot;

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2-eip&quot;
  })
}

resource &quot;aws_eip_association&quot; &quot;app&quot; {
  instance_id   = aws_instance.app.id
  allocation_id = aws_eip.app.id
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic IP를 사용하면 EC2의 Public IP를 고정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 주의할 점이 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Elastic IP는 public IP 고정에 사용한다.
private IP 고정 문제를 해결하는 리소스는 아니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 사용하지 않는 Elastic IP는 비용이 발생할 수 있으므로 실습 후 정리해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. EC2 replacement와 private IP 변경 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2를 Terraform으로 운영하다 보면 중요한 문제를 만날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 EC2 replacement다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서 어떤 변경은 기존 EC2를 그대로 수정하는 방식으로 처리된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;in-place update&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 어떤 변경은 기존 EC2를 삭제하고 새 EC2를 만드는 방식으로 처리된다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;replacement&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform plan에서 replacement는 보통 다음처럼 보인다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;-/+ aws_instance.app must be replaced&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 기존 EC2가 사라지고 새 EC2가 생성될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 EC2가 새로 생성되면서 동적으로 할당된 값들이 바뀔 수 있다는 점이다.&lt;/p&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;instance_id
private_ip
public_ip
network_interface_id&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.1 실제로 생길 수 있는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 EC2가 자동 할당 private IP를 사용한다고 하자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;기존 EC2 private IP
10.0.1.25&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 리소스나 설정이 이 IP를 직접 바라보고 있을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;다른 서버 설정
backend = 10.0.1.25&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 IAM Instance Profile, 네트워크 설정, AMI, User Data 교체 설정 등으로 EC2 replacement가 발생하면 새 EC2가 생성된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;새 EC2 private IP
10.0.1.87&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 기존 IP를 바라보던 리소스는 새 EC2를 찾지 못한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EC2 replacement
&amp;rarr; private IP 변경
&amp;rarr; 기존 연결 대상이 사라짐
&amp;rarr; 서비스 연결 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 핵심은 다음이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;동적으로 할당된 private IP를 안정적인 연결 지점처럼 사용하면 위험하다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2 해결 방법 1: private_ip 고정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 방법은 EC2에 private IP를 명시하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;app&quot; {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.private.id
  vpc_security_group_ids = [aws_security_group.app.id]

  private_ip = &quot;10.0.2.10&quot;

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 EC2가 재생성될 때 같은 private IP를 다시 사용하려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 주의할 점이 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;기존 EC2가 해당 IP를 아직 점유 중이면
새 EC2 생성이 실패할 수 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 고정 private IP는 같은 IP 유지에는 유리하지만, 무중단 교체에는 불리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.3 해결 방법 2: ENI를 별도 리소스로 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 명확한 방식은 private IP를 EC2가 아니라 ENI에 묶는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ENI는 Elastic Network Interface의 약자로, EC2에 연결되는 네트워크 인터페이스다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_network_interface&quot; &quot;app&quot; {
  subnet_id       = aws_subnet.private.id
  private_ips     = [&quot;10.0.2.10&quot;]
  security_groups = [aws_security_group.app.id]

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-app-eni&quot;
  })
}

resource &quot;aws_instance&quot; &quot;app&quot; {
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = var.instance_type

  iam_instance_profile = aws_iam_instance_profile.ec2.name

  network_interface {
    network_interface_id = aws_network_interface.app.id
    device_index         = 0
  }

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 네트워크 정체성과 컴퓨팅 리소스를 분리한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;네트워크 정체성
&amp;rarr; ENI

서버 실행 리소스
&amp;rarr; EC2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 ENI를 주 네트워크 인터페이스로 사용하는 경우에도 attach/detach 순서와 replacement 동작을 잘 확인해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.4 해결 방법 3: IP 대신 DNS 또는 Load Balancer 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하면 다른 리소스가 EC2 private IP를 직접 바라보지 않게 하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2의 private IP는 EC2가 교체되거나 재생성될 때 바뀔 수 있다. 따라서 다른 리소스가 private IP를 직접 참조하면 EC2 replacement 시 연결이 깨질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;위험한 구조:
Other Service &amp;rarr; 10.0.2.10

EC2 replacement 발생:
10.0.2.10 &amp;rarr; 10.0.2.35

결과:
Other Service는 여전히 10.0.2.10을 바라봄&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 줄이려면 IP를 직접 참조하기보다, 중간에 안정적인 연결 지점을 두는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;권장 구조:
Other Service &amp;rarr; DNS / Load Balancer / Service Discovery &amp;rarr; EC2&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;11.4.1 DNS를 사용하는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 DNS를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Route53 Private Hosted Zone을 사용하면 내부 도메인 이름을 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;app.internal.example.com
&amp;rarr; EC2 private IP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_route53_record&quot; &quot;app&quot; {
  zone_id = aws_route53_zone.private.zone_id
  name    = &quot;app.internal.example.com&quot;
  type    = &quot;A&quot;
  ttl     = 60
  records = [aws_instance.app.private_ip]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 다른 리소스는 EC2의 private IP를 직접 알 필요가 없다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Before:
Other Service &amp;rarr; 10.0.2.10

After:
Other Service &amp;rarr; app.internal.example.com&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2가 교체되어 private IP가 바뀌더라도, 다른 리소스의 설정을 모두 수정하는 대신 DNS Record만 갱신하면 된다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;기존:
app.internal.example.com &amp;rarr; 10.0.2.10

변경:
app.internal.example.com &amp;rarr; 10.0.2.35&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 DNS 방식도 완전한 즉시 전환을 보장하지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS에는 TTL과 캐시가 있기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DNS Record 변경
&amp;rarr; 일부 클라이언트는 기존 IP 캐시 사용
&amp;rarr; TTL 이후 새 IP 조회&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 TTL이 60초라면, 일부 클라이언트는 최대 60초 정도 기존 IP를 계속 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 DNS는 IP 직접 참조를 줄이는 데는 좋지만, 즉시 전환이나 무중단 배포가 필요한 경우에는 한계가 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;DNS는 IP 변경의 영향을 줄여주지만, TTL과 캐시 때문에 즉시 전환을 보장하지는 않는다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;11.4.2 Load Balancer를 사용하는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 안정적인 방식은 EC2 앞에 Load Balancer를 두는 것이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Other Service
&amp;rarr; Internal ALB / NLB
&amp;rarr; EC2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 다른 리소스가 EC2를 직접 바라보지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 리소스는 Load Balancer를 바라보고, Load Balancer가 뒤쪽의 EC2로 트래픽을 전달한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Before:
Other Service &amp;rarr; EC2 private IP

After:
Other Service &amp;rarr; Internal Load Balancer &amp;rarr; EC2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 EC2가 교체되어도 클라이언트의 연결 지점이 유지된다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2가 새로 생성되면 Target Group에 새 EC2를 등록하고, 기존 EC2를 제거하면 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;기존 EC2
&amp;rarr; Target Group에서 제거

새 EC2
&amp;rarr; Target Group에 등록

Other Service
&amp;rarr; 여전히 Load Balancer만 바라봄&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Load Balancer는 Health Check를 통해 정상 상태의 대상에게만 트래픽을 보낼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;정상 EC2
&amp;rarr; 트래픽 전달

비정상 EC2
&amp;rarr; 트래픽 제외&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 무중단 배포, 장애 대응, 여러 EC2로의 트래픽 분산이 필요하다면 DNS만 사용하는 것보다 Load Balancer 구조가 더 적합하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;11.4.3 DNS와 Load Balancer 비교&lt;/h4&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; DNS &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; Load Balancer &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;td&gt;IP를 이름으로 추상화&lt;/td&gt;
&lt;td&gt;트래픽을 대상 리소스로 분산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 IP 변경 대응&lt;/td&gt;
&lt;td&gt;DNS Record 갱신 필요&lt;/td&gt;
&lt;td&gt;Target Group 대상 교체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전환 속도&lt;/td&gt;
&lt;td&gt;TTL / 캐시 영향 있음&lt;/td&gt;
&lt;td&gt;Health Check 기반으로 비교적 빠르게 전환 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health Check&lt;/td&gt;
&lt;td&gt;기본 DNS만으로는 어려움&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용&lt;/td&gt;
&lt;td&gt;상대적으로 낮음&lt;/td&gt;
&lt;td&gt;추가 비용 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;적합한 경우&lt;/td&gt;
&lt;td&gt;단순 내부 이름 관리&lt;/td&gt;
&lt;td&gt;무중단, 확장성, 장애 대응이 필요한 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 DNS와 Load Balancer는 둘 다 EC2 private IP 직접 참조를 줄이는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 목적과 안정성 수준이 다르다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;DNS
&amp;rarr; IP를 이름으로 추상화
&amp;rarr; 단순하고 비용 부담이 적음
&amp;rarr; TTL / 캐시 고려 필요

Load Balancer
&amp;rarr; EC2 교체를 뒤에서 흡수
&amp;rarr; Health Check 가능
&amp;rarr; 무중단 배포와 확장에 유리
&amp;rarr; 비용 증가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소규모 내부 도구나 단순 연결에는 DNS만으로 충분할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서비스 안정성, 무중단 배포, 장애 대응이 중요하다면 Load Balancer를 사용하는 것이 더 적합하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;IP를 직접 참조하지 말고, DNS나 Load Balancer 같은 안정적인 연결 지점을 두는 것이 좋다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.5 해결 방법 4: prevent_destroy로 실수 방지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도치 않은 EC2 교체를 막고 싶다면 &lt;code&gt;prevent_destroy&lt;/code&gt;를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;app&quot; {
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = var.instance_type

  lifecycle {
    prevent_destroy = true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정은 Terraform이 해당 리소스를 삭제하려고 할 때 오류를 발생시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 이것은 구조적인 해결책이라기보다 안전장치에 가깝다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;prevent_destroy
&amp;rarr; 실수 방지용

DNS / LB / ENI 분리
&amp;rarr; 구조적 해결책&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.6 IAM 변경은 profile 교체보다 policy 변경 중심으로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에 연결된 Instance Profile 자체를 자주 바꾸면 EC2 변경 영향이 커질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하면 EC2에 붙는 Instance Profile은 안정적으로 유지하고, 권한 변경은 Role에 연결된 Policy를 수정하는 방식이 더 안전하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;권장 방향:
EC2 &amp;rarr; 같은 Instance Profile 유지
Role Policy 변경으로 권한 조정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음처럼 Role 자체는 유지하고 Policy Attachment를 변경한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role_policy_attachment&quot; &quot;s3_read&quot; {
  role       = aws_iam_role.ec2.name
  policy_arn = aws_iam_policy.s3_read.arn
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 어떤 변경이 실제로 replacement를 유발하는지는 Terraform Provider 버전과 리소스 구성에 따라 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 항상 &lt;code&gt;terraform plan&lt;/code&gt;에서 다음 표시를 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;~   in-place update
-/+ replacement&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. EC2 의존성 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 여러 리소스를 참조한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;VPC
&amp;rarr; Subnet
&amp;rarr; Security Group
&amp;rarr; IAM Instance Profile
&amp;rarr; Key Pair
&amp;rarr; EC2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 의존성은 트리보다 그래프에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Decision Options Flow-2026-05-12-064821.png&quot; data-origin-width=&quot;3626&quot; data-origin-height=&quot;1465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0sKY5/dJMcagZKNmi/xiYTzLia1FfQreDP3xTfK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0sKY5/dJMcagZKNmi/xiYTzLia1FfQreDP3xTfK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0sKY5/dJMcagZKNmi/xiYTzLia1FfQreDP3xTfK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0sKY5%2FdJMcagZKNmi%2FxiYTzLia1FfQreDP3xTfK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3626&quot; height=&quot;1465&quot; data-filename=&quot;Decision Options Flow-2026-05-12-064821.png&quot; data-origin-width=&quot;3626&quot; data-origin-height=&quot;1465&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 Subnet, Security Group, AMI, Key Pair, Instance Profile을 모두 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 EC2를 단순 서버 하나로 보기보다 여러 리소스가 결합된 실행 리소스로 보는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. 자주 하는 실수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13.1 Public IP만 있으면 인터넷이 된다고 생각함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Public IP는 인터넷 통신을 위한 조건 중 하나일 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 조건이 함께 필요하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Public IP
Internet Gateway
Route Table
Security Group
NACL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Public IP가 있어도 Route Table에 Internet Gateway 경로가 없으면 인터넷 통신이 되지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13.2 Security Group에서 SSH를 전체 공개함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH를 전체 인터넷에 열어두는 것은 위험하다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 권장하지 않음
cidr_ipv4 = &quot;0.0.0.0/0&quot;
from_port = 22
to_port   = 22&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하면 본인의 IP만 허용한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;cidr_ipv4 = &quot;1.2.3.4/32&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 좋은 방식은 SSH 대신 SSM Session Manager를 사용하는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13.3 Access Key를 EC2에 직접 넣음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에서 AWS API를 호출해야 한다고 해서 Access Key를 코드나 환경 변수에 직접 넣으면 안 된다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 권장하지 않음
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 IAM Role과 Instance Profile을 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13.4 User Data를 배포 도구처럼 사용함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User Data는 초기 부팅 스크립트에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 EC2의 애플리케이션을 반복적으로 배포하는 도구로 사용하기에는 한계가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복 배포가 필요하다면 CI/CD, SSM Run Command, 이미지 빌드, 구성 관리 도구 등을 고려하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13.5 EC2 private IP를 고정 식별자로 사용함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2의 private IP를 명시하지 않으면 AWS가 자동으로 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2 replacement가 발생하면 private IP가 바뀔 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다른 리소스가 EC2 private IP를 직접 바라보는 구조는 주의해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;위험한 구조:
Other Service &amp;rarr; EC2 private IP 직접 참조

더 나은 구조:
Other Service &amp;rarr; DNS / Load Balancer / Service Discovery&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13.6 terraform plan에서 replacement를 확인하지 않음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform plan에서 다음 표시는 매우 중요하다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;~   in-place update
-/+ replacement&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에 &lt;code&gt;-/+&lt;/code&gt;가 표시된다면 새 EC2가 생성되고 기존 EC2가 삭제될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 IP, instance id, attached resource 영향까지 확인해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13.7 실습 후 EC2를 삭제하지 않음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 실행 중이면 비용이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습 후에는 반드시 삭제한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;terraform destroy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elastic IP, EBS Volume도 함께 정리되었는지 확인하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;14. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Terraform으로 EC2를 구현하는 방법을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 단순히 서버 하나를 만드는 리소스처럼 보이지만, 실제로는 여러 요소가 함께 연결된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;AMI
Instance Type
Subnet
Security Group
Key Pair
IAM Instance Profile
User Data
EBS
Public IP / Private IP&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Terraform에서는 EC2가 replacement될 수 있다는 점도 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2가 교체되면 private IP나 public IP가 바뀔 수 있고, 이를 직접 바라보던 다른 리소스가 연결을 잃을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 EC2를 안정적으로 운영하려면 다음 관점을 가져야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EC2 private IP를 직접 연결 대상으로 삼지 않는다.
가능하면 DNS, Load Balancer, Service Discovery를 사용한다.
IAM 권한은 Access Key가 아니라 Role로 부여한다.
terraform plan에서 replacement 여부를 반드시 확인한다.&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;한 줄 정리&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EC2는 서버처럼 보이지만, 네트워크&amp;middot;보안&amp;middot;권한&amp;middot;스토리지가 결합된 교체 가능한 리소스다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 S3를 Terraform으로 구현해본다. S3는 단순 저장소처럼 보이지만 Public Access Block, Versioning, Encryption, Lifecycle, Bucket Policy를 함께 고려해야 하는 리소스다.&lt;/p&gt;</description>
      <category>테라폼</category>
      <author>pininini</author>
      <guid isPermaLink="true">https://pininininfra.tistory.com/17</guid>
      <comments>https://pininininfra.tistory.com/17#entry17comment</comments>
      <pubDate>Tue, 12 May 2026 15:45:19 +0900</pubDate>
    </item>
    <item>
      <title>4-4. 테라폼 - IAM Role과 Policy 구현하기</title>
      <link>https://pininininfra.tistory.com/16</link>
      <description>&lt;h1 style=&quot;text-align: center;&quot;&gt;테라폼 - IAM Role과 Policy 구현하기&lt;/h1&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;AWS 리소스가 어떤 권한으로 동작할지 Terraform으로 정의하기&lt;/i&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 Security Group을 통해 AWS 리소스 간 네트워크 접근 관계를 정의하는 방법을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 AWS 권한 관리의 핵심인 &lt;b&gt;IAM Role&lt;/b&gt;과 &lt;b&gt;Policy&lt;/b&gt;를 Terraform으로 구현해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS를 사용하다 보면 다음과 같은 상황을 자주 만나게 된다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;EC2가 S3에 접근해야 한다.
Lambda가 CloudWatch Logs에 로그를 남겨야 한다.
ECS Task가 ECR에서 이미지를 pull 해야 한다.
애플리케이션이 Secrets Manager 값을 읽어야 한다.
GitHub Actions가 Terraform apply를 실행해야 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 필요한 것이 IAM이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;IAM은 AWS에서 &amp;ldquo;누가 무엇을 할 수 있는가&amp;rdquo;를 정의하는 권한 시스템이다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. IAM이란 무엇인가&lt;/li&gt;
&lt;li&gt;2. IAM User, Role, Policy 차이&lt;/li&gt;
&lt;li&gt;3. Trust Policy와 Permission Policy&lt;/li&gt;
&lt;li&gt;4. Terraform에서 IAM 작성 방식&lt;/li&gt;
&lt;li&gt;5. 기본 예제: EC2 Role 만들기&lt;/li&gt;
&lt;li&gt;6. EC2 Instance Profile&lt;/li&gt;
&lt;li&gt;7. Lambda Role 만들기&lt;/li&gt;
&lt;li&gt;8. ECS Task Execution Role과 Task Role&lt;/li&gt;
&lt;li&gt;9. Managed Policy와 Inline Policy&lt;/li&gt;
&lt;li&gt;10. data aws_iam_policy_document 사용하기&lt;/li&gt;
&lt;li&gt;11. IAM 의존성 흐름&lt;/li&gt;
&lt;li&gt;12. 자주 하는 실수&lt;/li&gt;
&lt;li&gt;13. 마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. IAM이란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM은 Identity and Access Management의 약자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에서 사용자, 애플리케이션, 리소스가 어떤 작업을 할 수 있는지 제어하는 서비스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같은 권한을 정의할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;EC2가 S3 객체를 읽을 수 있다.
Lambda가 CloudWatch Logs에 로그를 쓸 수 있다.
ECS Task가 Secrets Manager에서 Secret을 읽을 수 있다.
GitHub Actions가 Terraform을 실행할 수 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 질문은 두 가지다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 누가 이 권한을 사용할 수 있는가?
2. 이 권한으로 무엇을 할 수 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM을 제대로 이해하려면 이 두 질문을 분리해서 봐야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;IAM은 &amp;ldquo;누가 사용할 수 있는가&amp;rdquo;와 &amp;ldquo;무엇을 할 수 있는가&amp;rdquo;를 분리해서 설계해야 한다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. IAM User, Role, Policy 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM을 처음 접하면 User, Role, Policy가 헷갈릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 정리하면 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM User&lt;/td&gt;
&lt;td&gt;사람 또는 장기 자격 증명을 가진 사용자&lt;/td&gt;
&lt;td&gt;관리자 계정, 개발자 계정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM Role&lt;/td&gt;
&lt;td&gt;특정 주체가 임시로 사용할 수 있는 권한 묶음&lt;/td&gt;
&lt;td&gt;EC2 Role, Lambda Role, ECS Task Role&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM Policy&lt;/td&gt;
&lt;td&gt;허용하거나 거부할 작업을 정의한 문서&lt;/td&gt;
&lt;td&gt;S3 읽기, CloudWatch Logs 쓰기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 가능하면 IAM User의 Access Key를 직접 사용하는 것보다 Role을 사용하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 EC2 안에 Access Key를 넣는 방식은 권장하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 권장하지 않음
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 EC2에 IAM Role을 붙이면 EC2가 해당 Role의 권한으로 AWS API를 호출할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EC2
&amp;rarr; IAM Instance Profile
&amp;rarr; IAM Role
&amp;rarr; Permission Policy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 애플리케이션 코드에 Access Key를 직접 넣지 않아도 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Trust Policy와 Permission Policy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM Role을 이해할 때 가장 중요한 개념은 두 가지다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Trust Policy
Permission Policy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘을 구분하지 못하면 IAM이 매우 헷갈린다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 Trust Policy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trust Policy는 &lt;b&gt;누가 이 Role을 사용할 수 있는가&lt;/b&gt;를 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 EC2가 사용할 Role이라면 다음처럼 작성한다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;Service&quot;: &quot;ec2.amazonaws.com&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRole&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 의미는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;EC2 서비스가 이 Role을 Assume 할 수 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서는 &lt;code&gt;assume_role_policy&lt;/code&gt;에 작성한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;ec2&quot; {
  name = &quot;demo-ec2-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ec2.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Permission Policy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Permission Policy는 &lt;b&gt;이 Role이 무엇을 할 수 있는가&lt;/b&gt;를 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 S3 객체 읽기 권한은 다음처럼 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;s3:GetObject&quot;
      ],
      &quot;Resource&quot;: &quot;arn:aws:s3:::my-bucket/*&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 의미는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;특정 S3 Bucket 안의 객체를 읽을 수 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;질문&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust Policy&lt;/td&gt;
&lt;td&gt;누가 Role을 사용할 수 있는가?&lt;/td&gt;
&lt;td&gt;EC2, Lambda, ECS Task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission Policy&lt;/td&gt;
&lt;td&gt;Role이 무엇을 할 수 있는가?&lt;/td&gt;
&lt;td&gt;S3 읽기, 로그 쓰기, Secret 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Trust Policy는 &amp;ldquo;누가 사용할 수 있는가&amp;rdquo;, Permission Policy는 &amp;ldquo;무엇을 할 수 있는가&amp;rdquo;를 정의한다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Terraform에서 IAM 작성 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서 IAM을 작성할 때 자주 사용하는 리소스는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;aws_iam_role
aws_iam_policy
aws_iam_role_policy
aws_iam_role_policy_attachment
aws_iam_instance_profile&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;리소스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_iam_role&lt;/td&gt;
&lt;td&gt;Role 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_iam_policy&lt;/td&gt;
&lt;td&gt;재사용 가능한 Policy 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_iam_role_policy&lt;/td&gt;
&lt;td&gt;Role에 inline policy 직접 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_iam_role_policy_attachment&lt;/td&gt;
&lt;td&gt;Role에 managed policy 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aws_iam_instance_profile&lt;/td&gt;
&lt;td&gt;EC2에 Role을 연결하기 위한 프로필&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. IAM Role 생성
2. Trust Policy 설정
3. Permission Policy 생성
4. Role에 Policy 연결
5. 필요한 리소스에 Role 연결&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 기본 예제: EC2 Role 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 EC2가 S3 객체를 읽을 수 있는 Role을 만들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;EC2가 IAM Role을 사용한다.
EC2는 특정 S3 Bucket의 객체를 읽을 수 있다.
Access Key는 EC2 내부에 저장하지 않는다.&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 variables.tf&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;project_name&quot; {
  description = &quot;Project name&quot;
  type        = string
}

variable &quot;bucket_name&quot; {
  description = &quot;S3 bucket name to allow read access&quot;
  type        = string
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 IAM Role&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;ec2&quot; {
  name = &quot;${var.project_name}-ec2-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ec2.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2-role&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Role은 EC2가 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 Trust Policy에 다음 Principal이 있기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Service = &quot;ec2.amazonaws.com&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 IAM Policy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 EC2 Role이 사용할 권한을 만든다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_iam_policy&quot; &quot;s3_read&quot; {
  name        = &quot;${var.project_name}-s3-read-policy&quot;
  description = &quot;Allow read access to specific S3 bucket&quot;

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;s3:GetObject&quot;
        ]
        Resource = &quot;arn:aws:s3:::${var.bucket_name}/*&quot;
      },
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;s3:ListBucket&quot;
        ]
        Resource = &quot;arn:aws:s3:::${var.bucket_name}&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-s3-read-policy&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;s3:GetObject&lt;/code&gt;와 &lt;code&gt;s3:ListBucket&lt;/code&gt;은 Resource 범위가 다르다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;s3:GetObject
&amp;rarr; arn:aws:s3:::bucket-name/*

s3:ListBucket
&amp;rarr; arn:aws:s3:::bucket-name&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 권한을 작성할 때 이 차이를 자주 실수한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4 Role에 Policy 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Policy를 만들었다고 해서 Role이 자동으로 그 권한을 가지는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Role에 Policy를 attach 해야 한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role_policy_attachment&quot; &quot;ec2_s3_read&quot; {
  role       = aws_iam_role.ec2.name
  policy_arn = aws_iam_policy.s3_read.arn
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 EC2 Role은 해당 S3 읽기 권한을 가진다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;IAM Role
&amp;rarr; IAM Policy attachment
&amp;rarr; S3 읽기 권한&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. EC2 Instance Profile&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에 IAM Role을 붙이려면 한 가지 리소스가 더 필요하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;aws_iam_instance_profile&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 Role을 직접 붙이는 것이 아니라 Instance Profile을 통해 Role을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EC2
&amp;rarr; Instance Profile
&amp;rarr; IAM Role
&amp;rarr; Permission Policy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;resource &quot;aws_iam_instance_profile&quot; &quot;ec2&quot; {
  name = &quot;${var.project_name}-ec2-profile&quot;
  role = aws_iam_role.ec2.name
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 EC2 리소스에서 다음처럼 사용한다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;resource &quot;aws_instance&quot; &quot;app&quot; {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = &quot;t3.micro&quot;
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.ec2.id]

  iam_instance_profile = aws_iam_instance_profile.ec2.name

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 EC2 내부 애플리케이션은 Access Key 없이도 Role 권한으로 AWS API를 호출할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;EC2에서 AWS API를 호출해야 한다면 Access Key를 넣지 말고 IAM Role을 붙이는 것이 좋다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Lambda Role 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lambda도 실행되기 위해 IAM Role이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lambda Role의 Trust Policy는 EC2와 다르다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Service = &quot;lambda.amazonaws.com&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lambda가 사용할 Role은 다음처럼 만든다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;lambda&quot; {
  name = &quot;${var.project_name}-lambda-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;lambda.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-lambda-role&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lambda가 CloudWatch Logs에 로그를 남기려면 기본 로그 권한이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예제에서는 AWS Managed Policy를 붙일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role_policy_attachment&quot; &quot;lambda_basic&quot; {
  role       = aws_iam_role.lambda.name
  policy_arn = &quot;arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Lambda 리소스에서 Role ARN을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_lambda_function&quot; &quot;app&quot; {
  function_name = &quot;${var.project_name}-lambda&quot;
  role          = aws_iam_role.lambda.arn

  handler = &quot;index.handler&quot;
  runtime = &quot;nodejs20.x&quot;

  filename = &quot;lambda.zip&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 Lambda Role은 다음 흐름으로 이해하면 된다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Lambda
&amp;rarr; IAM Role
&amp;rarr; CloudWatch Logs 권한
&amp;rarr; 필요한 AWS API 권한&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. ECS Task Execution Role과 Task Role&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECS를 사용할 때는 Role이 두 개 나오는 경우가 많다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;ECS Task Execution Role
ECS Task Role&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 매우 헷갈릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘의 차이는 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task Execution Role&lt;/td&gt;
&lt;td&gt;ECS가 Task를 실행하기 위해 사용하는 Role&lt;/td&gt;
&lt;td&gt;ECR 이미지 pull, CloudWatch Logs 쓰기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task Role&lt;/td&gt;
&lt;td&gt;컨테이너 안의 애플리케이션이 사용하는 Role&lt;/td&gt;
&lt;td&gt;S3 접근, Secrets Manager 읽기, SQS 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Execution Role은 ECS가 쓰는 권한이고, Task Role은 애플리케이션이 쓰는 권한이다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 ECS Task Execution Role&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECS Task Execution Role의 Trust Policy는 다음 Principal을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Service = &quot;ecs-tasks.amazonaws.com&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;ecs_task_execution&quot; {
  name = &quot;${var.project_name}-ecs-task-execution-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ecs-tasks.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ecs-task-execution-role&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 ECS Task 실행에 필요한 AWS Managed Policy를 붙일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role_policy_attachment&quot; &quot;ecs_task_execution&quot; {
  role       = aws_iam_role.ecs_task_execution.name
  policy_arn = &quot;arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Role은 주로 다음 작업에 사용된다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;ECR에서 이미지 pull
CloudWatch Logs에 로그 전송
일부 Secret/Parameter 주입&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 ECS Task Role&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task Role은 컨테이너 안에서 실행되는 애플리케이션이 사용하는 Role이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 애플리케이션이 S3에 접근해야 한다면 Task Role에 S3 권한을 부여한다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role&quot; &quot;ecs_task&quot; {
  name = &quot;${var.project_name}-ecs-task-role&quot;

  assume_role_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Principal = {
          Service = &quot;ecs-tasks.amazonaws.com&quot;
        }
        Action = &quot;sts:AssumeRole&quot;
      }
    ]
  })

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ecs-task-role&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 필요한 권한을 붙인다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_iam_policy&quot; &quot;ecs_task_s3_read&quot; {
  name = &quot;${var.project_name}-ecs-task-s3-read-policy&quot;

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = [
          &quot;s3:GetObject&quot;
        ]
        Resource = &quot;arn:aws:s3:::${var.bucket_name}/*&quot;
      }
    ]
  })
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;ecs_task_s3_read&quot; {
  role       = aws_iam_role.ecs_task.name
  policy_arn = aws_iam_policy.ecs_task_s3_read.arn
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task Definition에서는 두 Role을 구분해서 넣는다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_ecs_task_definition&quot; &quot;app&quot; {
  family                   = &quot;${var.project_name}-app&quot;
  requires_compatibilities = [&quot;FARGATE&quot;]
  network_mode             = &quot;awsvpc&quot;
  cpu                      = 256
  memory                   = 512

  execution_role_arn = aws_iam_role.ecs_task_execution.arn
  task_role_arn      = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name  = &quot;app&quot;
      image = &quot;nginx:latest&quot;
    }
  ])
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이를 이해하면 ECS 권한 설계가 훨씬 쉬워진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Managed Policy와 Inline Policy&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM Policy를 Role에 부여하는 방식은 크게 두 가지로 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Managed Policy
Inline Policy&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.1 Managed Policy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Managed Policy는 독립적인 Policy 리소스로 만들고, Role에 attach해서 사용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_iam_policy&quot; &quot;s3_read&quot; {
  name = &quot;${var.project_name}-s3-read-policy&quot;

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = [&quot;s3:GetObject&quot;]
        Resource = &quot;arn:aws:s3:::${var.bucket_name}/*&quot;
      }
    ]
  })
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;s3_read&quot; {
  role       = aws_iam_role.ec2.name
  policy_arn = aws_iam_policy.s3_read.arn
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 Role에서 재사용할 수 있고, 권한을 독립적으로 관리하기 쉽다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.2 Inline Policy&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Inline Policy는 특정 Role에 직접 붙는 Policy다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_iam_role_policy&quot; &quot;ec2_s3_read_inline&quot; {
  name = &quot;${var.project_name}-ec2-s3-read-inline&quot;
  role = aws_iam_role.ec2.id

  policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = [&quot;s3:GetObject&quot;]
        Resource = &quot;arn:aws:s3:::${var.bucket_name}/*&quot;
      }
    ]
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Inline Policy는 해당 Role에 강하게 종속된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 권한을 특정 Role에만 붙이고 싶을 때 사용할 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; Managed Policy &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt; Inline Policy &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재사용&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;td&gt;어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관리 단위&lt;/td&gt;
&lt;td&gt;Policy 리소스&lt;/td&gt;
&lt;td&gt;Role 내부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용 예&lt;/td&gt;
&lt;td&gt;공통 권한&lt;/td&gt;
&lt;td&gt;특정 Role 전용 권한&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보 단계에서는 Managed Policy 방식부터 익히는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. data aws_iam_policy_document 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 &lt;code&gt;jsonencode&lt;/code&gt;로 Policy를 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서는 &lt;code&gt;aws_iam_policy_document&lt;/code&gt; data source를 사용해서 Policy 문서를 만들 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 EC2 Trust Policy를 다음처럼 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;data &quot;aws_iam_policy_document&quot; &quot;ec2_assume_role&quot; {
  statement {
    effect = &quot;Allow&quot;

    principals {
      type        = &quot;Service&quot;
      identifiers = [&quot;ec2.amazonaws.com&quot;]
    }

    actions = [&quot;sts:AssumeRole&quot;]
  }
}

resource &quot;aws_iam_role&quot; &quot;ec2&quot; {
  name               = &quot;${var.project_name}-ec2-role&quot;
  assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 읽기 정책도 다음처럼 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;data &quot;aws_iam_policy_document&quot; &quot;s3_read&quot; {
  statement {
    effect = &quot;Allow&quot;

    actions = [
      &quot;s3:GetObject&quot;
    ]

    resources = [
      &quot;arn:aws:s3:::${var.bucket_name}/*&quot;
    ]
  }

  statement {
    effect = &quot;Allow&quot;

    actions = [
      &quot;s3:ListBucket&quot;
    ]

    resources = [
      &quot;arn:aws:s3:::${var.bucket_name}&quot;
    ]
  }
}

resource &quot;aws_iam_policy&quot; &quot;s3_read&quot; {
  name   = &quot;${var.project_name}-s3-read-policy&quot;
  policy = data.aws_iam_policy_document.s3_read.json
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 JSON을 직접 작성하는 것보다 Terraform 문법에 가깝고, Policy가 길어질수록 가독성이 좋아질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 초보자라면 처음에는 &lt;code&gt;jsonencode&lt;/code&gt; 방식으로 IAM Policy 구조를 이해한 뒤, 나중에 &lt;code&gt;aws_iam_policy_document&lt;/code&gt;로 넘어가도 충분하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. IAM 의존성 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM Role과 Policy의 의존성 흐름을 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;IAM Role
├── Trust Policy
├── Permission Policy
└── Policy Attachment&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2의 경우 Instance Profile이 추가된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Permission Policy
&amp;rarr; IAM Role
&amp;rarr; Instance Profile
&amp;rarr; EC2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lambda의 경우 Role ARN을 직접 사용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Permission Policy
&amp;rarr; IAM Role
&amp;rarr; Lambda&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECS의 경우 Role이 두 종류로 나뉜다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Task Execution Role
&amp;rarr; ECS가 Task 실행에 사용

Task Role
&amp;rarr; 컨테이너 애플리케이션이 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 EC2의 경우이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Decision Options Flow-2026-05-12-060040.png&quot; data-origin-width=&quot;5115&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdTUCX/dJMcaffxZo5/bBNwrrkgwhI7FS1Iuhkssk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdTUCX/dJMcaffxZo5/bBNwrrkgwhI7FS1Iuhkssk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdTUCX/dJMcaffxZo5/bBNwrrkgwhI7FS1Iuhkssk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdTUCX%2FdJMcaffxZo5%2FbBNwrrkgwhI7FS1Iuhkssk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;5115&quot; height=&quot;305&quot; data-filename=&quot;Decision Options Flow-2026-05-12-060040.png&quot; data-origin-width=&quot;5115&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 Role 자체만 만든다고 권한이 완성되는 것이 아니라는 점이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Role 생성
&amp;rarr; Trust Policy 필요
&amp;rarr; Permission Policy 필요
&amp;rarr; 대상 리소스에 연결 필요&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 자주 하는 실수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.1 Trust Policy와 Permission Policy를 헷갈림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trust Policy는 누가 Role을 사용할 수 있는지를 정의한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Permission Policy는 Role이 무엇을 할 수 있는지를 정의한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Trust Policy
&amp;rarr; 누가 AssumeRole 할 수 있는가

Permission Policy
&amp;rarr; Assume한 뒤 무엇을 할 수 있는가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘은 반드시 구분해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.2 EC2에 Role만 만들고 Instance Profile을 만들지 않음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 IAM Role을 직접 붙이지 않고 Instance Profile을 통해 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 EC2용 Role을 만들었다면 Instance Profile도 만들어야 한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;aws_iam_role
&amp;rarr; aws_iam_instance_profile
&amp;rarr; aws_instance&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.3 Access Key를 EC2나 코드에 직접 넣음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2, Lambda, ECS에서 AWS API를 호출해야 한다고 해서 Access Key를 코드에 직접 넣으면 안 된다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 권장하지 않음
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 IAM Role을 사용해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.4 Resource를 너무 넓게 줌&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음처럼 모든 리소스에 대한 권한을 주면 위험하다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Resource = &quot;*&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하면 필요한 리소스 ARN만 지정한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Resource = &quot;arn:aws:s3:::my-bucket/*&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.5 Action을 너무 넓게 줌&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음처럼 서비스 전체 권한을 주는 것은 편하지만 위험하다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Action = &quot;s3:*&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하면 필요한 Action만 허용한다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;Action = [
  &quot;s3:GetObject&quot;,
  &quot;s3:ListBucket&quot;
]&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.6 ECS Execution Role과 Task Role을 헷갈림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECS에서 가장 흔한 실수 중 하나다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Execution Role
&amp;rarr; ECS가 이미지 pull, 로그 전송 등에 사용

Task Role
&amp;rarr; 컨테이너 애플리케이션이 AWS API 호출에 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 S3나 Secrets Manager에 접근해야 한다면 Task Role에 권한을 줘야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.7 IAM 변경이 EC2 교체로 이어질 수 있음을 놓침&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2에 연결된 IAM Instance Profile이나 관련 구성을 변경할 때, 상황에 따라 Terraform이 EC2를 교체하려고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2가 교체되면 instance id, public IP, private IP 등이 바뀔 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 EC2의 private IP를 다른 리소스가 직접 바라보는 구조라면 연결이 깨질 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;EC2 교체
&amp;rarr; private IP 변경
&amp;rarr; 기존 IP를 바라보던 리소스 연결 실패&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 이후 EC2 글에서 더 자세히 다룰 예정이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.8 권한 전파 시간을 고려하지 않음&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM 권한은 변경 직후 모든 곳에 즉시 반영되지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Role이나 Policy를 생성한 직후 바로 Lambda 실행, ECS Task 실행 등을 수행하면 권한 전파 지연으로 실패하는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 경우 CI/CD에서 짧은 대기나 재시도 로직을 두는 것이 도움이 될 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;13. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Terraform으로 IAM Role과 Policy를 구현하는 방법을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;IAM Role은 권한을 담는 그릇이다.
Trust Policy는 누가 Role을 사용할 수 있는지 정의한다.
Permission Policy는 Role이 무엇을 할 수 있는지 정의한다.
EC2는 Instance Profile을 통해 Role을 사용한다.
Lambda는 Role ARN을 직접 사용한다.
ECS는 Execution Role과 Task Role을 구분해야 한다.
Access Key를 코드에 넣지 말고 Role을 사용해야 한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM은 Terraform에서 가장 중요하면서도 실수하기 쉬운 영역이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 복잡하게 느껴지지만, 다음 문장만 기억하면 훨씬 이해하기 쉬워진다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;누가 사용할 수 있는가?
무엇을 할 수 있는가?&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;한 줄 정리&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IAM은 &amp;ldquo;누가 사용할 수 있는가&amp;rdquo;와 &amp;ldquo;무엇을 할 수 있는가&amp;rdquo;를 분리해서 설계해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 EC2를 Terraform으로 구현해본다. EC2는 단순 서버처럼 보이지만 Subnet, Security Group, IAM Role, Storage가 함께 연결되는 리소스다.&lt;/p&gt;</description>
      <category>테라폼</category>
      <author>pininini</author>
      <guid isPermaLink="true">https://pininininfra.tistory.com/16</guid>
      <comments>https://pininininfra.tistory.com/16#entry16comment</comments>
      <pubDate>Tue, 12 May 2026 14:53:23 +0900</pubDate>
    </item>
    <item>
      <title>4-3. 테라폼 - Security Group과 네트워크 보안 설정</title>
      <link>https://pininininfra.tistory.com/15</link>
      <description>&lt;h1 style=&quot;text-align: center;&quot;&gt;테라폼 - Security Group과 네트워크 보안 설정&lt;/h1&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;AWS 리소스 간 접근 관계를 Terraform 코드로 표현하기&lt;/i&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 VPC, Subnet, Internet Gateway, Route Table을 Terraform으로 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 AWS 네트워크 보안의 기본이 되는 &lt;b&gt;Security Group&lt;/b&gt;을 다뤄보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group은 흔히 방화벽이라고 설명되지만, Terraform으로 구현할 때는 단순히 포트를 여는 리소스로만 보면 안 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Security Group은 &quot;누가 누구에게 접근할 수 있는가&quot;를 정의하는 리소스다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Security Group은 단순 포트 설정이 아니라 리소스 간 접근 관계를 코드로 표현하는 도구에 가깝다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. Security Group이란 무엇인가&lt;/li&gt;
&lt;li&gt;2. Security Group의 기본 개념&lt;/li&gt;
&lt;li&gt;3. Ingress와 Egress&lt;/li&gt;
&lt;li&gt;4. CIDR 허용과 Security Group 참조&lt;/li&gt;
&lt;li&gt;5. Terraform에서 Security Group 작성 방식&lt;/li&gt;
&lt;li&gt;6. 기본 예제: EC2용 Security Group&lt;/li&gt;
&lt;li&gt;7. 실전 예제: ALB &amp;rarr; EC2 구조&lt;/li&gt;
&lt;li&gt;8. 실전 예제: EC2 &amp;rarr; RDS 구조&lt;/li&gt;
&lt;li&gt;9. Security Group과 NACL의 차이&lt;/li&gt;
&lt;li&gt;10. Security Group 의존성 흐름&lt;/li&gt;
&lt;li&gt;11. 자주 하는 실수&lt;/li&gt;
&lt;li&gt;12. 마무리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Security Group이란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group은 AWS 리소스에 적용하는 가상 방화벽이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2, RDS, ALB, ECS Task, Lambda VPC 연결 같은 리소스에 붙어서 inbound와 outbound 트래픽을 제어한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Inbound  &amp;rarr; 들어오는 트래픽
Outbound &amp;rarr; 나가는 트래픽&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 EC2에 Security Group을 연결하면, 그 EC2로 들어오는 SSH, HTTP 요청을 허용하거나 차단할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;SSH   22번 포트
HTTP  80번 포트
HTTPS 443번 포트
MySQL 3306번 포트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Security Group을 단순히 포트 허용 목록으로만 이해하면 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 다음처럼 리소스 간 접근 관계를 정의하는 데 더 많이 사용한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;사용자 &amp;rarr; ALB
ALB &amp;rarr; EC2 또는 ECS
EC2 또는 ECS &amp;rarr; RDS
Lambda &amp;rarr; RDS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 중요한 질문은 이것이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;어떤 포트를 열 것인가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 더 중요한 질문은 다음이다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;누가 누구에게 접근할 수 있어야 하는가?&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Security Group의 기본 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group을 이해할 때 꼭 알아야 할 개념이 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 VPC에 속한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group은 VPC 안에 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Security Group을 만들 때는 VPC ID가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;app&quot; {
  name        = &quot;app-sg&quot;
  description = &quot;Security group for app&quot;
  vpc_id      = aws_vpc.main.id
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;vpc_id = aws_vpc.main.id&lt;/code&gt;를 통해 Security Group이 VPC를 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Security Group은 앞선 분류 기준으로 보면 &lt;b&gt;Dependent&lt;/b&gt;에 해당한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Security Group
&amp;rarr; VPC 필요
&amp;rarr; Dependent&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Stateful이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group은 Stateful하게 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 허용된 요청에 대한 응답 트래픽은 별도의 반대 방향 rule이 없어도 허용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 EC2가 외부 API로 요청을 보냈다면, 그 응답은 inbound rule에 명시적으로 열려 있지 않아도 돌아올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;EC2 &amp;rarr; 외부 API 요청
외부 API &amp;rarr; EC2 응답

응답 트래픽은 자동 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 점은 뒤에서 설명할 Network ACL과 다른 부분이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Security Group은 Stateful, Network ACL은 Stateless로 이해하면 쉽다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 여러 개를 붙일 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 리소스에 여러 Security Group을 연결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 각 Security Group의 rule은 합쳐져서 적용된다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;EC2
├── common-sg
├── ssh-sg
└── app-sg&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 초보 단계에서는 하나의 리소스에 너무 많은 Security Group을 붙이면 오히려 이해하기 어려워질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 역할별로 명확하게 나누는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ALB용 Security Group
App용 Security Group
DB용 Security Group&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Ingress와 Egress&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group rule은 크게 두 가지로 나뉜다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;Ingress = 들어오는 트래픽
Egress  = 나가는 트래픽&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 Ingress&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ingress는 리소스로 들어오는 트래픽을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 EC2에 SSH 접속을 허용하려면 inbound rule을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;내 IP
&amp;rarr; EC2 22번 포트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청을 허용하려면 80번 포트를 연다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;사용자
&amp;rarr; EC2 80번 포트&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Egress&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Egress는 리소스에서 나가는 트래픽을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 EC2가 외부 패키지 저장소에서 파일을 다운로드하거나, 애플리케이션이 외부 API를 호출하는 경우 outbound 트래픽이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;EC2
&amp;rarr; 외부 API
&amp;rarr; 패키지 저장소
&amp;rarr; AWS API&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group을 새로 만들면 보통 outbound는 전체 허용으로 시작하는 경우가 많다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;All outbound traffic 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 보안이 중요한 환경에서는 outbound도 필요한 대상만 허용하도록 제한할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. CIDR 허용과 Security Group 참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group rule을 작성할 때 가장 중요한 선택이 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;IP 대역으로 허용할 것인가?
Security Group을 참조할 것인가?&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 CIDR 허용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CIDR 허용은 특정 IP 대역에서 들어오는 트래픽을 허용하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 내 IP에서만 SSH를 허용하려면 다음처럼 설정한다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;1.2.3.4/32 &amp;rarr; EC2 22번 포트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 코드로는 다음처럼 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;ec2_ssh&quot; {
  security_group_id = aws_security_group.ec2.id

  cidr_ipv4   = var.allowed_ssh_cidr
  ip_protocol = &quot;tcp&quot;
  from_port   = 22
  to_port     = 22
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CIDR 방식은 외부 사용자나 관리자 IP처럼 고정된 네트워크 대역을 허용할 때 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 내부 리소스 간 통신에는 Security Group 참조 방식이 더 좋은 경우가 많다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 Security Group 참조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group 참조는 IP가 아니라 Security Group 자체를 source로 사용하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 ALB에서 EC2로 8080 포트 접근을 허용한다고 하자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ALB Security Group
&amp;rarr; EC2 Security Group
&amp;rarr; 8080 포트 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 ALB의 IP가 바뀌어도 문제가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 IP가 아니라 &amp;ldquo;ALB Security Group을 가진 리소스만 접근 가능하다&amp;rdquo;는 관계이기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;source = ALB Security Group&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 코드로는 다음처럼 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;app_from_alb&quot; {
  security_group_id = aws_security_group.app.id

  referenced_security_group_id = aws_security_group.alb.id
  ip_protocol                  = &quot;tcp&quot;
  from_port                    = 8080
  to_port                      = 8080
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 실무에서 매우 중요하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;내부 리소스 간 통신은 IP보다 Security Group 참조로 표현하는 것이 안정적이다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Terraform에서 Security Group 작성 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform에서 Security Group을 작성하는 방식은 크게 두 가지가 있다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;1. aws_security_group 안에 ingress / egress를 inline으로 작성
2. rule 리소스를 별도로 분리해서 작성&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 Inline 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예제에서는 다음처럼 &lt;code&gt;aws_security_group&lt;/code&gt; 안에 rule을 직접 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;ec2&quot; {
  name   = &quot;ec2-sg&quot;
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = &quot;tcp&quot;
    cidr_blocks = [var.allowed_ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = &quot;-1&quot;
    cidr_blocks = [&quot;0.0.0.0/0&quot;]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 초보자가 이해하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 rule이 많아지면 관리가 어려워진다. 또한 rule 변경 시 의도하지 않은 diff가 생길 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 Rule 분리 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 Security Group 자체와 rule을 분리해서 작성하는 방식을 추천한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;aws_security_group
aws_vpc_security_group_ingress_rule
aws_vpc_security_group_egress_rule&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;ec2&quot; {
  name        = &quot;${var.project_name}-ec2-sg&quot;
  description = &quot;Security group for EC2&quot;
  vpc_id      = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2-sg&quot;
  })
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;ec2_ssh&quot; {
  security_group_id = aws_security_group.ec2.id

  cidr_ipv4   = var.allowed_ssh_cidr
  ip_protocol = &quot;tcp&quot;
  from_port   = 22
  to_port     = 22
}

resource &quot;aws_vpc_security_group_egress_rule&quot; &quot;ec2_all&quot; {
  security_group_id = aws_security_group.ec2.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;-1&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 장점은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Security Group 본체와 rule을 분리해서 관리할 수 있다.
rule 단위로 변경 사항을 추적하기 쉽다.
복잡한 구조에서 가독성이 좋아진다.
리소스 간 참조 관계를 더 명확하게 표현할 수 있다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 초보자도 실무 방식에 익숙해질 수 있도록 rule 분리 방식을 기준으로 설명한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 기본 예제: EC2용 Security Group&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 가장 기본적인 EC2용 Security Group을 만들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;내 IP에서만 SSH 허용
모든 곳에서 HTTP 허용
Outbound 전체 허용&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 variables.tf&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;variable &quot;project_name&quot; {
  description = &quot;Project name&quot;
  type        = string
}

variable &quot;allowed_ssh_cidr&quot; {
  description = &quot;CIDR block allowed to access EC2 via SSH&quot;
  type        = string
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 terraform.tfvars&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;project_name     = &quot;demo&quot;
allowed_ssh_cidr = &quot;1.2.3.4/32&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;allowed_ssh_cidr&lt;/code&gt;에는 본인의 공인 IP를 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 중이라도 SSH를 &lt;code&gt;0.0.0.0/0&lt;/code&gt;으로 열어두는 것은 권장하지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 Security Group 본체&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;ec2&quot; {
  name        = &quot;${var.project_name}-ec2-sg&quot;
  description = &quot;Security group for EC2&quot;
  vpc_id      = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-ec2-sg&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group은 VPC 안에 생성되므로 &lt;code&gt;vpc_id&lt;/code&gt;가 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.4 SSH Ingress Rule&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;ec2_ssh&quot; {
  security_group_id = aws_security_group.ec2.id

  cidr_ipv4   = var.allowed_ssh_cidr
  ip_protocol = &quot;tcp&quot;
  from_port   = 22
  to_port     = 22

  description = &quot;Allow SSH from my IP&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 rule은 내 IP에서만 EC2의 22번 포트로 접근할 수 있게 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.5 HTTP Ingress Rule&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;ec2_http&quot; {
  security_group_id = aws_security_group.ec2.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;tcp&quot;
  from_port   = 80
  to_port     = 80

  description = &quot;Allow HTTP from anywhere&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 rule은 누구나 EC2의 80번 포트로 접근할 수 있게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서버 실습용으로는 사용할 수 있지만, 운영 환경에서는 ALB를 앞에 두고 EC2는 ALB에서만 접근하도록 제한하는 것이 일반적이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.6 Egress Rule&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_vpc_security_group_egress_rule&quot; &quot;ec2_all&quot; {
  security_group_id = aws_security_group.ec2.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;-1&quot;

  description = &quot;Allow all outbound traffic&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 rule은 EC2에서 외부로 나가는 모든 트래픽을 허용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자 예제에서는 보통 이렇게 시작하지만, 운영 환경에서는 필요한 outbound만 허용하도록 제한할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 실전 예제: ALB &amp;rarr; EC2 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영에 가까운 웹 서비스 구조에서는 사용자가 EC2에 직접 접근하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 ALB를 앞에 두고, EC2는 ALB에서 들어오는 요청만 허용한다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;User
&amp;rarr; ALB
&amp;rarr; EC2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Security Group은 다음처럼 나누는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ALB Security Group
&amp;rarr; 사용자 HTTP/HTTPS 허용

EC2 Security Group
&amp;rarr; ALB Security Group에서 오는 트래픽만 허용&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 ALB Security Group&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;alb&quot; {
  name        = &quot;${var.project_name}-alb-sg&quot;
  description = &quot;Security group for ALB&quot;
  vpc_id      = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-alb-sg&quot;
  })
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;alb_http&quot; {
  security_group_id = aws_security_group.alb.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;tcp&quot;
  from_port   = 80
  to_port     = 80

  description = &quot;Allow HTTP from anywhere&quot;
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;alb_https&quot; {
  security_group_id = aws_security_group.alb.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;tcp&quot;
  from_port   = 443
  to_port     = 443

  description = &quot;Allow HTTPS from anywhere&quot;
}

resource &quot;aws_vpc_security_group_egress_rule&quot; &quot;alb_all&quot; {
  security_group_id = aws_security_group.alb.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;-1&quot;

  description = &quot;Allow all outbound traffic&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 EC2 Security Group&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2는 외부 전체가 아니라 ALB Security Group에서 오는 요청만 허용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;app&quot; {
  name        = &quot;${var.project_name}-app-sg&quot;
  description = &quot;Security group for app EC2&quot;
  vpc_id      = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-app-sg&quot;
  })
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;app_from_alb&quot; {
  security_group_id = aws_security_group.app.id

  referenced_security_group_id = aws_security_group.alb.id
  ip_protocol                  = &quot;tcp&quot;
  from_port                    = 8080
  to_port                      = 8080

  description = &quot;Allow app traffic from ALB&quot;
}

resource &quot;aws_vpc_security_group_egress_rule&quot; &quot;app_all&quot; {
  security_group_id = aws_security_group.app.id

  cidr_ipv4   = &quot;0.0.0.0/0&quot;
  ip_protocol = &quot;-1&quot;

  description = &quot;Allow all outbound traffic&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조의 장점은 EC2가 외부에 직접 노출되지 않는다는 점이다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;잘못된 구조:
Internet &amp;rarr; EC2

권장 구조:
Internet &amp;rarr; ALB &amp;rarr; EC2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EC2의 IP가 바뀌어도 ALB Security Group을 source로 허용했기 때문에 관계가 유지된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;내부 연결은 IP보다 Security Group 참조로 표현하는 것이 더 안정적이다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실전 예제: EC2 &amp;rarr; RDS 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 애플리케이션 서버가 RDS에 접근하는 구조를 보자.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;EC2 또는 ECS
&amp;rarr; RDS MySQL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 RDS는 외부 전체에 열면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS Security Group은 애플리케이션 Security Group에서 오는 3306 포트만 허용하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 App Security Group&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;app&quot; {
  name        = &quot;${var.project_name}-app-sg&quot;
  description = &quot;Security group for app&quot;
  vpc_id      = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-app-sg&quot;
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 RDS Security Group&lt;/h3&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;resource &quot;aws_security_group&quot; &quot;rds&quot; {
  name        = &quot;${var.project_name}-rds-sg&quot;
  description = &quot;Security group for RDS&quot;
  vpc_id      = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-rds-sg&quot;
  })
}

resource &quot;aws_vpc_security_group_ingress_rule&quot; &quot;rds_from_app&quot; {
  security_group_id = aws_security_group.rds.id

  referenced_security_group_id = aws_security_group.app.id
  ip_protocol                  = &quot;tcp&quot;
  from_port                    = 3306
  to_port                      = 3306

  description = &quot;Allow MySQL from app&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 App Security Group을 가진 리소스만 RDS의 3306 포트에 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS를 다음처럼 열어두는 것은 위험하다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 권장하지 않음
cidr_ipv4 = &quot;0.0.0.0/0&quot;
from_port = 3306
to_port   = 3306&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB는 가능한 한 Private Subnet에 두고, 접근 대상도 App Security Group으로 제한하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Security Group과 NACL의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS VPC에서는 Security Group 외에도 &lt;b&gt;Network ACL&lt;/b&gt;, 줄여서 &lt;b&gt;NACL&lt;/b&gt;이라는 네트워크 보안 리소스가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 네트워크 트래픽을 제어하지만 적용 위치와 동작 방식이 다르다.&lt;/p&gt;
&lt;table style=&quot;height: 145px;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;Security Group&lt;/td&gt;
&lt;td style=&quot;height: 17px; text-align: center;&quot;&gt;NACL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;적용 위치&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;EC2, ALB, RDS, ENI 등 리소스 단위&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Subnet 단위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;동작 방식&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Stateful&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Stateless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Rule&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Allow만 가능&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Allow / Deny 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;평가 방식&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;모든 rule을 종합&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Rule 번호 순서대로 평가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;주 사용처&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;리소스 간 접근 제어&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;Subnet 전체 트래픽 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.1 Security Group은 Stateful&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group은 Stateful하게 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 허용된 요청에 대한 응답 트래픽은 자동으로 허용된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client &amp;rarr; EC2 80번 포트 요청 허용
EC2 &amp;rarr; Client 응답 자동 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일반적인 서비스 접근 제어는 Security Group만으로도 충분한 경우가 많다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.2 NACL은 Stateless&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NACL은 Stateless하게 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청과 응답을 각각 따로 허용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client &amp;rarr; EC2 80번 포트 허용
EC2 &amp;rarr; Client 응답 포트도 별도 허용 필요&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 NACL을 잘못 설정하면 Security Group은 맞게 열려 있는데도 통신이 되지 않는 상황이 생길 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.3 NACL은 언제 사용할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보 단계에서는 대부분 Security Group 중심으로 설계해도 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NACL은 보통 다음과 같은 경우에 추가로 고려한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;특정 IP 대역을 Subnet 전체에서 차단
Public Subnet / Private Subnet 단위의 보안 경계 설정
보안 정책상 Deny rule이 필요한 경우
Subnet 전체 트래픽을 제어해야 하는 경우&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 특정 악성 IP를 Subnet 전체에서 차단하고 싶다면 NACL이 더 적합할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Deny 1.2.3.4/32&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group은 Deny rule이 없고 Allow rule만 가능하기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.4 Terraform에서 NACL 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NACL은 Terraform에서 다음 리소스로 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;resource &quot;aws_network_acl&quot; &quot;public&quot; {
  vpc_id = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = &quot;${var.project_name}-public-nacl&quot;
  })
}

resource &quot;aws_network_acl_rule&quot; &quot;allow_http_inbound&quot; {
  network_acl_id = aws_network_acl.public.id

  rule_number = 100
  egress      = false
  protocol    = &quot;tcp&quot;
  rule_action = &quot;allow&quot;
  cidr_block  = &quot;0.0.0.0/0&quot;
  from_port   = 80
  to_port     = 80
}

resource &quot;aws_network_acl_association&quot; &quot;public&quot; {
  subnet_id      = aws_subnet.public.id
  network_acl_id = aws_network_acl.public.id
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 NACL은 rule 번호, inbound/outbound 양방향 허용, ephemeral port 등을 함께 고려해야 해서 초보 단계에서는 복잡하게 느껴질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 글에서는 NACL을 깊게 다루기보다는 Security Group과의 차이만 이해하고 넘어간다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Security Group은 리소스 단위 방화벽이고, NACL은 Subnet 단위 방화벽이다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. Security Group 의존성 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group은 VPC에 속하고, rule은 Security Group을 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Security Group 간 참조를 사용하면 리소스 간 관계가 더 명확해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 ALB &amp;rarr; EC2 &amp;rarr; RDS 구조는 다음과 같이 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Decision Options Flow-2026-05-12-051550.png&quot; data-origin-width=&quot;1755&quot; data-origin-height=&quot;2045&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHnfJD/dJMcafmh1OD/B5wve83szf2yAq8JxooJDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHnfJD/dJMcafmh1OD/B5wve83szf2yAq8JxooJDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHnfJD/dJMcafmh1OD/B5wve83szf2yAq8JxooJDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHnfJD%2FdJMcafmh1OD%2FB5wve83szf2yAq8JxooJDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;350&quot; data-filename=&quot;Decision Options Flow-2026-05-12-051550.png&quot; data-origin-width=&quot;1755&quot; data-origin-height=&quot;2045&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 IP 주소가 아니라 Security Group 간 관계로 접근을 정의했다는 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;보안 규칙은 IP 중심보다 역할 중심으로 작성하는 것이 좋다.&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 자주 하는 실수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.1 SSH를 0.0.0.0/0으로 열기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 중이라도 SSH를 전체 인터넷에 열어두는 것은 위험하다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 권장하지 않음
cidr_ipv4 = &quot;0.0.0.0/0&quot;
from_port = 22
to_port   = 22&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하면 본인의 IP만 허용한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;cidr_ipv4 = &quot;1.2.3.4/32&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 좋은 방식은 EC2에 직접 SSH를 열지 않고 SSM Session Manager를 사용하는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2 RDS를 전체 공개하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS의 3306 포트를 전체 공개하면 매우 위험하다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 권장하지 않음
cidr_ipv4 = &quot;0.0.0.0/0&quot;
from_port = 3306
to_port   = 3306&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS는 App Security Group에서만 접근 가능하도록 제한하는 것이 좋다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.3 내부 리소스도 CIDR로만 관리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 리소스 간 통신을 CIDR로만 관리하면 구조 변경에 취약해진다.&lt;/p&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;10.0.1.0/24 &amp;rarr; 3306 허용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 해당 Subnet 전체에서 접근할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Security Group 참조를 사용하면 특정 역할을 가진 리소스만 접근하도록 제한할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;App Security Group &amp;rarr; RDS Security Group&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.4 Inline rule과 별도 rule 리소스를 섞어 쓰기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Security Group에 inline rule과 별도 rule 리소스를 섞어 쓰면 관리가 헷갈릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 Security Group에 대해서는 가능하면 한 가지 방식으로 통일하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;권장:
aws_security_group
+
aws_vpc_security_group_ingress_rule
+
aws_vpc_security_group_egress_rule&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.5 Outbound를 생각하지 않기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자는 inbound만 신경 쓰는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 애플리케이션이 외부 API를 호출하거나 AWS API를 호출하려면 outbound도 필요하다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;App &amp;rarr; RDS
App &amp;rarr; 외부 API
App &amp;rarr; Secrets Manager
App &amp;rarr; SSM Parameter Store&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안이 중요한 환경에서는 outbound도 필요한 대상만 허용하도록 설계해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.6 Security Group 이름에 의존하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terraform 코드에서는 Security Group 이름보다 ID를 참조하는 것이 일반적이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;vpc_security_group_ids = [aws_security_group.app.id]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름은 중복되거나 변경될 수 있지만, Terraform에서는 리소스 참조를 통해 정확한 ID를 사용할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.7 Security Group과 NACL을 동시에 수정하고 원인을 헷갈림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통신이 안 될 때 Security Group만 확인하고 NACL을 보지 않거나, 반대로 NACL만 보고 Security Group을 놓치는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 발생하면 다음 순서로 확인하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;1. Route Table 경로가 맞는가?
2. Security Group inbound / outbound가 맞는가?
3. NACL inbound / outbound가 맞는가?
4. 대상 리소스가 실제로 해당 포트에서 listen 중인가?
5. ALB / Target Group Health Check가 맞는가?&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Terraform으로 Security Group을 구현하는 방법을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Security Group은 VPC에 속한다.
Ingress는 들어오는 트래픽이다.
Egress는 나가는 트래픽이다.
외부 접근은 CIDR로 제한한다.
내부 리소스 간 접근은 Security Group 참조를 사용한다.
Security Group 본체와 rule은 분리해서 관리하는 것이 좋다.
NACL은 Subnet 단위 보조 방어 계층으로 이해하면 된다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Security Group은 단순히 포트를 여는 리소스가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프라 안에서 어떤 리소스가 어떤 리소스에 접근할 수 있는지를 표현하는 중요한 보안 경계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NACL도 함께 존재하지만, 초보 단계에서는 Security Group을 중심으로 이해하고, NACL은 Subnet 전체 단위의 추가 방어 계층으로 이해하면 충분하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;한 줄 정리&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Security Group은 포트를 여는 도구가 아니라, 리소스 간 접근 관계를 정의하는 도구다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 IAM Role과 Policy를 다룬다. IAM은 AWS 리소스가 어떤 권한으로 동작할 수 있는지를 결정하는 핵심 리소스다.&lt;/p&gt;</description>
      <category>테라폼</category>
      <author>pininini</author>
      <guid isPermaLink="true">https://pininininfra.tistory.com/15</guid>
      <comments>https://pininininfra.tistory.com/15#entry15comment</comments>
      <pubDate>Tue, 12 May 2026 14:18:39 +0900</pubDate>
    </item>
  </channel>
</rss>