Cloud & DevOps |

Infrastructure as Code with Terraform: 수동 인프라 관리에서 코드형 인프라로의 여정

AWS 콘솔 클릭 지옥에서 벗어나 Terraform으로 인프라를 코드화한 실전 경험. HCL 문법부터 GitOps 워크플로우까지 스토리텔링으로 풀어냅니다.

By SouvenirList
Infrastructure as Code with Terraform: 수동 인프라 관리에서 코드형 인프라로의 여정

Infrastructure as Code with Terraform: 수동 인프라 관리에서 코드형 인프라로의 여정

모든 좋은 기술 도입에는 이야기가 있습니다. 이 글은 댄 하먼의 8단 스토리 서클(Story Circle) 구조를 따라, AWS 콘솔에서 수동으로 인프라를 관리하던 시절부터 Terraform 기반의 GitOps 워크플로우를 구축하기까지의 여정을 기술적으로 풀어냅니다.


TL;DR

  • 수동 인프라 관리는 초기에는 빠르지만, 규모가 커지면 환경 불일치, 재현 불가, 휴먼 에러의 원인이 됩니다
  • Terraform은 HCL 언어로 인프라를 선언적으로 정의하고, Plan/Apply 사이클로 변경을 안전하게 적용하는 IaC 도구입니다
  • Module, Workspace, Remote State는 Terraform을 프로덕션 레벨로 운영하기 위한 핵심 패턴입니다
  • State 관리, Drift Detection, 비밀 관리 등 운영 과제를 해결하고 CI/CD와 통합하면 인프라 관리의 패러다임이 근본적으로 바뀝니다

1단계: 안정 (You) — AWS 콘솔에서 클릭하던 시절

모든 이야기는 주인공의 일상에서 시작됩니다.

우리 팀의 인프라 관리도 마찬가지였습니다. AWS Management Console에 로그인해서 EC2 인스턴스를 만들고, Security Group을 설정하고, RDS를 프로비저닝하는 것이 일상이었습니다. 마우스 클릭 몇 번이면 서버가 뚝딱 만들어졌습니다.

인프라 요청 → AWS 콘솔 로그인 → 마우스 클릭 → 리소스 생성 → "됐다!"

팀 내부 위키에는 이런 문서가 가득했습니다:

## 신규 서버 생성 가이드 (v3.2)

1. AWS Console > EC2 > Launch Instance 클릭
2. AMI: Amazon Linux 2023 선택
3. Instance Type: t3.medium 선택
4. VPC: prod-vpc 선택 (주의: dev-vpc 아님!)
5. Security Group: sg-web-prod 선택
6. Storage: 50GB gp3 설정
7. Tags: Name=prod-web-XX, Environment=production
8. ⚠️ 중요: 반드시 "prod-subnet-az-a"에 배치할 것
9. ⚠️ 주의: IAM Role 빼먹지 말 것!

이 방식은 직관적이고 즉각적이었습니다. 클릭하면 바로 결과가 보이고, 문제가 있으면 콘솔에서 바로 수정하면 됐습니다. 모든 것이 편안한 컴포트 존(Comfort Zone) 이었습니다.

하지만 안정은 오래가지 않습니다.


2단계: 욕구 (Need) — 이대로는 안 된다

이야기의 주인공에게는 해결해야 할 문제가 생깁니다.

서비스가 성장하면서 인프라 규모가 커지자, 수동 관리의 약점이 한꺼번에 드러났습니다.

문제 1: 환경 불일치 (Environment Drift)

개발 환경과 운영 환경이 미묘하게 달랐습니다. “개발에서는 되는데 운영에서는 안 돼요”가 일상이 됐습니다.

[dev 환경]  Security Group: 포트 8080 오픈 ✅
[staging]   Security Group: 포트 8080 오픈 ✅
[prod 환경] Security Group: 포트 8080... 누가 안 열었네? ❌

원인: 3개월 전 dev에서 수동으로 열었는데, prod에는 반영 안 함

문제 2: 재현 불가능

장애 복구 시 “지난번에 어떻게 설정했더라?”가 반복됐습니다. 위키 문서는 이미 3버전 전 내용이었고, 실제 인프라와 일치하지 않았습니다.

문제 3: 휴먼 에러

금요일 오후 5시, 피곤한 상태에서 Production Security Group의 인바운드 규칙을 잘못 수정해 0.0.0.0/0으로 전체 포트를 열어버린 사건이 발생했습니다.

변경 이력:  "누가", "언제", "왜" 수정했는지 추적 불가
롤백 방법:  "이전 상태가 뭐였는지 아는 사람?"
감사 로그:  CloudTrail은 있지만... 의도까지는 기록되지 않음

시스템은 우리에게 분명히 말하고 있었습니다. “클릭으로는 한계가 있다.”


3단계: 진입 (Go) — Terraform이라는 낯선 세계

주인공은 익숙한 세계를 떠나 새로운 영역에 발을 들입니다.

우리는 HashiCorp의 Terraform이라는 낯선 세계에 진입하기로 결정했습니다.

Infrastructure as Code란?

인프라를 수동으로 관리하는 대신, 코드로 선언적으로 정의하고 버전 관리하는 방식입니다.

[수동 관리]
엔지니어 → AWS 콘솔 → 마우스 클릭 → 리소스 생성
          (과정이 머릿속에만 존재)

[Infrastructure as Code]
엔지니어 → .tf 파일 작성 → terraform plan → terraform apply → 리소스 생성
          (과정이 코드로 존재, Git에 이력 보존)

첫 번째 Terraform 코드

떨리는 마음으로 첫 번째 .tf 파일을 작성했습니다:

# provider.tf - AWS와의 연결 설정
terraform {
  required_version = ">= 1.7.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-2"  # 서울 리전

  default_tags {
    tags = {
      ManagedBy   = "terraform"
      Environment = var.environment
      Project     = "souvenirlist"
    }
  }
}
# main.tf - 첫 번째 리소스 정의
resource "aws_instance" "web" {
  ami           = "ami-0c6e5afdd23291f73"  # Amazon Linux 2023
  instance_type = "t3.medium"
  subnet_id     = aws_subnet.public_a.id

  vpc_security_group_ids = [aws_security_group.web.id]
  iam_instance_profile   = aws_iam_instance_profile.web.name

  root_block_device {
    volume_size = 50
    volume_type = "gp3"
  }

  tags = {
    Name = "${var.environment}-web-server"
  }
}

terraform plan을 처음 실행했을 때의 감동은 잊을 수 없습니다. 실제 적용 전에 무엇이 변경될지 미리 확인할 수 있다니. 콘솔에서 클릭하고 “어… 이거 맞나?” 하던 시절과는 차원이 달랐습니다.


4단계: 탐색 (Search) — HCL, State, Provider 학습

낯선 세계에서 주인공은 시행착오를 겪으며 적응합니다.

HCL 문법과 친해지기

HCL(HashiCorp Configuration Language)은 선언적 언어입니다. “어떻게”가 아니라 “무엇을” 원하는지 기술합니다.

# 변수 정의
variable "environment" {
  description = "배포 환경 (dev, staging, prod)"
  type        = string
  default     = "dev"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environment는 dev, staging, prod 중 하나여야 합니다."
  }
}

variable "instance_config" {
  description = "인스턴스 설정"
  type = map(object({
    instance_type = string
    min_size      = number
    max_size      = number
  }))

  default = {
    dev     = { instance_type = "t3.small",  min_size = 1, max_size = 2  }
    staging = { instance_type = "t3.medium", min_size = 2, max_size = 4  }
    prod    = { instance_type = "t3.large",  min_size = 3, max_size = 10 }
  }
}

# 로컬 값
locals {
  config    = var.instance_config[var.environment]
  is_prod   = var.environment == "prod"
  name_prefix = "souvenirlist-${var.environment}"
}

State 파일의 존재

Terraform의 핵심은 State 파일입니다. .tfstate는 “Terraform이 관리하는 인프라의 현재 상태”를 기록합니다.

┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│   .tf 파일       │     │   .tfstate       │     │   실제 AWS       │
│  (원하는 상태)    │     │  (알고 있는 상태) │     │  (실제 상태)     │
│                  │     │                  │     │                  │
│  instance_type:  │     │  instance_type:  │     │  instance_type:  │
│  "t3.large"     │ ←→  │  "t3.medium"     │ ←→  │  "t3.medium"     │
└──────────────────┘     └──────────────────┘     └──────────────────┘
        Plan 시: .tf와 .tfstate를 비교 → "t3.medium → t3.large 변경 필요"

처음에는 State 파일을 로컬에 두고 작업했습니다. 팀원이 한 명일 때는 괜찮았지만, 두 명이 동시에 terraform apply를 실행하는 순간 State 충돌이라는 지옥을 경험했습니다.

시행착오: 로컬 State의 비극

[팀원 A] terraform apply → State v1 → v2 (인스턴스 추가)
[팀원 B] terraform apply → State v1 → v2 (보안그룹 수정)

                              팀원 B의 State가 팀원 A의 변경사항을 덮어씀
                              → 팀원 A가 만든 인스턴스가 State에서 사라짐
                              → Terraform이 모르는 "유령 리소스" 탄생

이 경험이 Remote State의 필요성을 체감하게 해주었습니다.


5단계: 발견 (Find) — Module, Workspace, Remote State

시행착오 끝에 주인공은 목표에 도달합니다.

Remote State with S3 Backend

State 파일을 S3에 저장하고, DynamoDB로 잠금(Lock)을 관리하는 패턴을 도입했습니다:

# backend.tf
terraform {
  backend "s3" {
    bucket         = "souvenirlist-terraform-state"
    key            = "infrastructure/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}
# State 버킷과 Lock 테이블 자체도 코드로 관리 (별도 bootstrap 프로젝트)
resource "aws_s3_bucket" "terraform_state" {
  bucket = "souvenirlist-terraform-state"

  lifecycle {
    prevent_destroy = true  # 실수로 삭제 방지
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"  # State 이력 보존
  }
}

resource "aws_dynamodb_table" "terraform_lock" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

이제 팀원이 동시에 작업해도 DynamoDB Lock이 충돌을 방지합니다.

Module로 재사용 가능한 인프라 구성

같은 패턴의 인프라를 환경별로 반복 작성하는 것은 비효율적입니다. Module로 추상화했습니다:

modules/
  ├── networking/
  │   ├── main.tf        # VPC, Subnet, NAT Gateway
  │   ├── variables.tf   # 입력 변수
  │   └── outputs.tf     # 출력 값
  ├── compute/
  │   ├── main.tf        # EC2, ASG, ALB
  │   ├── variables.tf
  │   └── outputs.tf
  └── database/
      ├── main.tf        # RDS, ElastiCache
      ├── variables.tf
      └── outputs.tf

environments/
  ├── dev/
  │   └── main.tf        # dev 환경 설정
  ├── staging/
  │   └── main.tf        # staging 환경 설정
  └── prod/
      └── main.tf        # prod 환경 설정
# modules/compute/main.tf
resource "aws_launch_template" "app" {
  name_prefix   = "${var.name_prefix}-app-"
  image_id      = var.ami_id
  instance_type = var.instance_type

  network_interfaces {
    security_groups = var.security_group_ids
  }

  user_data = base64encode(templatefile("${path.module}/userdata.sh.tpl", {
    environment = var.environment
    app_port    = var.app_port
  }))

  tag_specifications {
    resource_type = "instance"
    tags = merge(var.common_tags, {
      Name = "${var.name_prefix}-app"
    })
  }
}

resource "aws_autoscaling_group" "app" {
  name                = "${var.name_prefix}-app-asg"
  min_size            = var.min_size
  max_size            = var.max_size
  desired_capacity    = var.desired_capacity
  vpc_zone_identifier = var.private_subnet_ids
  health_check_type   = "ELB"

  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }

  dynamic "tag" {
    for_each = var.common_tags
    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }
}
# environments/prod/main.tf - Module 호출
module "networking" {
  source = "../../modules/networking"

  environment    = "prod"
  vpc_cidr       = "10.0.0.0/16"
  azs            = ["ap-northeast-2a", "ap-northeast-2c"]
  public_cidrs   = ["10.0.1.0/24", "10.0.2.0/24"]
  private_cidrs  = ["10.0.10.0/24", "10.0.20.0/24"]
}

module "compute" {
  source = "../../modules/compute"

  environment        = "prod"
  name_prefix        = "souvenirlist-prod"
  ami_id             = data.aws_ami.app.id
  instance_type      = "t3.large"
  min_size           = 3
  max_size           = 10
  desired_capacity   = 3
  private_subnet_ids = module.networking.private_subnet_ids
  security_group_ids = [module.networking.app_sg_id]
  app_port           = 8080
}

Workspace로 멀티 환경 관리

Workspace를 활용하면 동일한 코드로 여러 환경을 관리할 수 있습니다:

# workspace 기반 환경 분리
locals {
  env_config = {
    dev = {
      instance_type    = "t3.small"
      min_size         = 1
      max_size         = 2
      db_instance_class = "db.t3.micro"
      enable_monitoring = false
    }
    staging = {
      instance_type    = "t3.medium"
      min_size         = 2
      max_size         = 4
      db_instance_class = "db.t3.small"
      enable_monitoring = true
    }
    prod = {
      instance_type    = "t3.large"
      min_size         = 3
      max_size         = 10
      db_instance_class = "db.r6g.large"
      enable_monitoring = true
    }
  }

  current = local.env_config[terraform.workspace]
}

resource "aws_instance" "app" {
  instance_type = local.current.instance_type
  # ... 나머지 설정
}
# Workspace 사용법
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

terraform workspace select prod
terraform plan    # prod 설정으로 Plan
terraform apply   # prod 환경에 적용

이제 terraform workspace select dev && terraform apply 한 줄로 개발 환경 전체를 프로비저닝할 수 있었습니다. 콘솔에서 수십 분 걸리던 작업이 3분으로 줄었습니다.


6단계: 대가 (Take) — 운영의 현실

원하는 것을 얻었지만, 주인공은 무거운 대가를 치릅니다.

Terraform은 공짜가 아니었습니다.

대가 1: State Lock과 State 오염

팀원이 terraform apply 중 강제 종료하면 DynamoDB Lock이 남아 다른 사람이 작업을 못 하는 상황이 발생했습니다.

# Lock 강제 해제 (신중하게!)
terraform force-unlock LOCK_ID

# 더 큰 문제: State가 중간 상태로 남은 경우
# 리소스는 생성됐는데 State에는 기록 안 됨
terraform import aws_instance.web i-0abc123def456

대가 2: Configuration Drift

누군가 콘솔에서 직접 리소스를 수정하면 Terraform이 모르는 변경이 생깁니다. 다음 terraform plan에서 의도치 않은 변경사항이 나타났습니다.

# Drift 감지
terraform plan -detailed-exitcode
# Exit code 0: 변경 없음
# Exit code 1: 에러
# Exit code 2: 변경 감지됨 (drift!)

# 실제 상태를 State에 반영 (코드를 실제에 맞추는 경우)
terraform refresh

# 또는 코드 상태를 실제에 적용 (실제를 코드에 맞추는 경우)
terraform apply

규칙을 세웠습니다: “콘솔에서 직접 수정하는 것은 장애 대응 시에만. 수정 후 반드시 코드에 반영할 것.”

대가 3: 비밀 관리의 어려움

데이터베이스 비밀번호를 .tf 파일에 하드코딩하면 Git에 비밀이 노출됩니다.

# 절대 하면 안 되는 것
resource "aws_db_instance" "main" {
  password = "SuperSecretPassword123!"  # Git에 비밀번호 노출!
}

# 올바른 방법: AWS Secrets Manager 연동
data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "souvenirlist/prod/db-password"
}

resource "aws_db_instance" "main" {
  engine               = "postgresql"
  engine_version       = "15.4"
  instance_class       = local.current.db_instance_class
  allocated_storage    = 100
  storage_encrypted    = true

  username = "app_admin"
  password = data.aws_secretsmanager_secret_version.db_password.secret_string

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.db.id]

  backup_retention_period = local.is_prod ? 30 : 7
  multi_az                = local.is_prod
  deletion_protection     = local.is_prod
}

7단계: 귀환 (Return) — CI/CD와 통합한 GitOps 워크플로우

대가를 치른 주인공은 원래 세계로 돌아옵니다.

인프라 코드를 Git으로 관리하게 되면서 자연스럽게 CI/CD 파이프라인과 통합하는 GitOps 워크플로우를 구축했습니다.

GitHub Actions 기반 Terraform Pipeline

# .github/workflows/terraform.yml
name: Terraform CI/CD

on:
  pull_request:
    paths: ['terraform/**']
  push:
    branches: [main]
    paths: ['terraform/**']

env:
  TF_VERSION: '1.7.5'
  AWS_REGION: 'ap-northeast-2'

jobs:
  plan:
    name: Terraform Plan
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Init
        run: terraform init -backend-config=backend.hcl
        working-directory: terraform/environments/prod

      - name: Terraform Format Check
        run: terraform fmt -check -recursive

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan
        working-directory: terraform/environments/prod

      - name: Comment Plan on PR
        uses: actions/github-script@v7
        with:
          script: |
            const plan = `${{ steps.plan.outputs.stdout }}`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Terraform Plan\n\`\`\`\n${plan}\n\`\`\``
            });

  apply:
    name: Terraform Apply
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Init
        run: terraform init -backend-config=backend.hcl
        working-directory: terraform/environments/prod

      - name: Terraform Apply
        run: terraform apply -auto-approve
        working-directory: terraform/environments/prod

워크플로우 전체 흐름

┌─────────────────────────────────────────────────────────────────┐
│                    GitOps Terraform Workflow                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. 개발자: feature branch에서 .tf 파일 수정                      │
│                    │                                             │
│                    ▼                                             │
│  2. Pull Request 생성                                            │
│                    │                                             │
│                    ▼                                             │
│  3. CI: terraform fmt → validate → plan                         │
│         Plan 결과가 PR 코멘트로 자동 게시                         │
│                    │                                             │
│                    ▼                                             │
│  4. 팀 리뷰: 코드 + Plan 결과 검토                                │
│         "이 변경이 맞나?" 확인 후 Approve                         │
│                    │                                             │
│                    ▼                                             │
│  5. main에 Merge                                                 │
│                    │                                             │
│                    ▼                                             │
│  6. CD: terraform apply (자동 적용)                               │
│         Slack 알림: "prod 인프라 변경 완료"                        │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

이제 인프라 변경은 코드 리뷰를 거치고, Plan 결과를 팀이 확인한 후에만 적용됩니다. 금요일 오후 5시에 콘솔에서 실수하는 일은 더 이상 없습니다.


8단계: 변화 (Change) — 인프라에 대한 패러다임 전환

여정을 마친 주인공은 근본적으로 변화합니다.

Terraform을 경험한 후, 인프라를 바라보는 관점 자체가 바뀌었습니다.

Before vs After: 사고방식의 변화

[Before] "서버 하나 추가해 주세요" → 콘솔 접속 → 클릭클릭
[After]  "서버 하나 추가해 주세요" → PR 올리고 → 리뷰 받고 → Merge

[Before] "이 서버 설정이 뭐였지?" → 콘솔에서 일일이 확인
[After]  "이 서버 설정이 뭐였지?" → Git에서 코드 확인

[Before] "개발 환경 하나 더 만들어 주세요" → 반나절 작업
[After]  "개발 환경 하나 더 만들어 주세요" → terraform workspace new → 3분

[Before] "누가 이거 바꿨어?" → CloudTrail 뒤지기
[After]  "누가 이거 바꿨어?" → git log

Pros & Cons

구분내용
Pros
재현 가능성동일한 코드로 동일한 인프라를 언제든 재생성 가능
버전 관리Git으로 모든 변경 이력 추적, 롤백 가능
코드 리뷰인프라 변경도 PR 리뷰를 거쳐 실수 방지
자동화CI/CD와 통합하여 반복 작업 제거
문서화코드 자체가 인프라 명세서 역할
Cons
학습 곡선HCL 문법, State 관리, Module 설계 등 초기 학습 필요
State 관리State 파일 오염, Lock 충돌 등 운영 부담
Drift 관리콘솔 직접 수정 시 코드와 실제 불일치 발생
비밀 관리민감 정보의 안전한 관리를 위한 추가 설계 필요
디버깅에러 메시지가 직관적이지 않은 경우가 많음

FAQ

Q: Terraform과 AWS CloudFormation 중 어떤 것을 선택해야 하나요?

AWS만 사용한다면 CloudFormation도 좋은 선택입니다. 하지만 멀티 클라우드를 고려하거나, AWS 외 SaaS(Datadog, GitHub, PagerDuty 등)도 코드로 관리하고 싶다면 Terraform이 더 유연합니다. Terraform의 Provider 생태계는 3,000개 이상의 서비스를 지원합니다.

Q: State 파일이 손상되면 어떻게 복구하나요?

S3 버킷에 버전 관리를 활성화해 두었다면 이전 버전의 State를 복원할 수 있습니다. 최악의 경우 terraform import로 기존 리소스를 하나씩 다시 State에 등록해야 합니다. 이런 상황을 방지하기 위해 State 버킷의 버전 관리와 백업은 필수입니다.

Q: 기존 수동 관리 리소스를 Terraform으로 옮기려면?

terraform import 명령으로 기존 리소스를 State에 등록한 후, 해당 리소스의 .tf 코드를 작성합니다. Terraform 1.5부터 import 블록을 사용하면 코드 생성까지 자동화할 수 있습니다.

Q: 모든 인프라를 하나의 State로 관리해야 하나요?

절대 아닙니다. 서비스 또는 레이어별로 State를 분리하는 것이 좋습니다. 네트워킹, 데이터베이스, 애플리케이션 인프라를 각각 별도의 State로 관리하면 blast radius를 줄이고 plan/apply 속도도 빨라집니다. terraform_remote_state data source로 State 간 값을 참조할 수 있습니다.


마치며

댄 하먼의 스토리 서클은 “변화는 대가를 수반한다” 는 것을 가르쳐줍니다.

Infrastructure as Code도 마찬가지입니다. 콘솔 클릭의 즉각적인 편안함이라는 컴포트 존을 떠나, 학습 곡선과 운영 복잡성이라는 대가를 치르고, 재현 가능성과 자동화라는 보상을 얻어 돌아옵니다. 그리고 그 여정을 통해 인프라와 팀 모두가 성장합니다.

중요한 것은, 첫날부터 완벽한 Module 구조와 GitOps 파이프라인을 갖출 필요가 없다는 점입니다. main.tf 하나에서 시작해 점진적으로 발전시키면 됩니다. Terraform의 진짜 가치는 도구 자체가 아니라, “인프라를 코드로 관리한다”는 사고방식의 전환에 있습니다.

여러분의 인프라는 지금 스토리 서클의 어느 단계에 있나요?


함께 읽으면 좋은 글

Tags: terraform infrastructure-as-code devops aws cloud gitops

Related Articles