13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] 인프라자동화

  • [Cloud] Pulumi vs Terraform 비교: IaC 도구 선택 가이드

    IaC 도구 선택, 왜 이렇게 어렵냐고요 😅

    팀에서 인프라 자동화 도구를 새로 도입하려고 할 때, 가장 먼저 나오는 질문이 있죠. “Terraform 쓸까요, Pulumi 쓸까요?”

    저도 몇 년 전에 이 선택 앞에서 한참 고민했거든요. 당시엔 Terraform이 거의 IaC(Infrastructure as Code, 코드로 인프라를 정의하고 관리하는 방식)의 표준처럼 여겨지던 시절이었는데, Pulumi라는 새로운 녀석이 등장하면서 “이거 써야 하나?” 싶었던 기억이 납니다.

    결론부터 말씀드리면 — 둘 다 써봤고, 둘 다 장단점이 뚜렷해요. Pulumi vs Terraform 비교는 단순히 “어느 게 더 좋냐”의 문제가 아니라, 팀 상황과 프로젝트 성격에 따라 달라지는 문제더라고요. 오늘은 13년 동안 인프라 엔지니어로 일하면서 직접 겪은 경험을 바탕으로, 이 두 IaC 도구를 제대로 비교해드리겠습니다.

    Pulumi와 Terraform의 전체적인 구조와 접근 방식 차이를 보여주는 개요 다이어그램

    Terraform과 Pulumi, 각각 어떤 도구인가요?

    Terraform — IaC의 베테랑

    Terraform은 HashiCorp에서 만든 오픈소스 IaC 도구로, 2014년에 처음 출시됐습니다. HCL(HashiCorp Configuration Language)이라는 자체 DSL(Domain-Specific Language, 특정 목적을 위해 만들어진 언어)을 사용하는 게 특징이에요.

    쉽게 말해서, “인프라를 선언적으로 정의”하는 방식입니다. “EC2 인스턴스 이렇게 만들어줘”라고 적어두면, Terraform이 알아서 현재 상태와 비교해서 필요한 작업만 수행하는 거죠.

    # Terraform 예시 — AWS EC2 인스턴스 생성
    resource "aws_instance" "web_server" {
      ami           = "ami-0c55b159cbfafe1f0"
      instance_type = "t3.micro"
    
      tags = {
        Name        = "web-server"
        Environment = "production"
      }
    }
    
    output "instance_ip" {
      value = aws_instance.web_server.public_ip
    }

    처음 보면 “이게 뭔 언어야?” 싶을 수 있는데, 몇 번 써보면 꽤 직관적이더라고요. 특히 인프라 구성을 “읽는” 관점에서는 진짜 편합니다.

    Pulumi — 개발자 친화적인 도전자

    Pulumi는 2018년에 등장한 비교적 젊은 IaC 도구입니다. 가장 큰 차별점은 실제 프로그래밍 언어를 그대로 사용한다는 점이에요. Python, TypeScript, Go, C#, Java 등을 지원하거든요.

    처음 이걸 봤을 때 “오, 이거 개발자들 좋아하겠다” 싶었어요. 인프라 엔지니어보다 소프트웨어 개발자 출신이 많은 팀이라면 특히요.

    # Pulumi 예시 — Python으로 AWS EC2 인스턴스 생성
    import pulumi
    import pulumi_aws as aws
    
    web_server = aws.ec2.Instance(
        "web-server",
        ami="ami-0c55b159cbfafe1f0",
        instance_type="t3.micro",
        tags={
            "Name": "web-server",
            "Environment": "production",
        }
    )
    
    pulumi.export("instance_ip", web_server.public_ip)

    같은 결과물인데, Python 개발자라면 아래 코드가 훨씬 익숙하게 느껴질 겁니다. 이게 Pulumi의 핵심 가치예요.

    Pulumi vs Terraform 핵심 차이점 비교

    자, 이제 본격적으로 비교해 봅시다. 제가 직접 두 도구를 써보면서 느낀 차이점들을 정리했어요.

    비교 항목 Terraform Pulumi
    언어 HCL (자체 DSL) Python, TypeScript, Go, C# 등
    학습 곡선 HCL은 쉽지만, 복잡한 로직은 어려움 언어는 익숙하지만 Pulumi 개념 학습 필요
    상태 관리 로컬 파일 또는 원격 백엔드 Pulumi Cloud 또는 자체 백엔드
    프로바이더 생태계 매우 방대 (성숙한 생태계) 성장 중 (Terraform 프로바이더 브리지 지원)
    테스트 제한적 (Terratest 등 별도 도구 필요) 언어 기본 테스트 프레임워크 활용 가능
    커뮤니티 매우 활발, 레퍼런스 풍부 성장 중
    라이선스 BSL 1.1 (v1.5.5부터 변경) Apache 2.0 (오픈소스)
    엔터프라이즈 기능 Terraform Cloud/Enterprise Pulumi Cloud

    ⚠️ 라이선스 변경 이슈 주의! Terraform은 2023년 8월에 라이선스를 MPL 2.0에서 BSL(Business Source License) 1.1로 변경했습니다. 이 때문에 OpenTofu라는 포크 프로젝트가 생겼을 정도로 커뮤니티에서 논란이 됐었어요. 상업적 목적으로 사용할 때는 라이선스 조건을 꼭 확인하세요.

    Terraform의 plan/apply 워크플로우와 Pulumi의 preview/up 워크플로우를 나란히 비교한 다이어그램

    실전에서 느끼는 차이 — 직접 써보니까요

    Terraform이 빛나는 순간들

    제가 Terraform을 처음 도입했을 때가 2019년쯤이었는데, 당시 팀에 인프라 엔지니어가 저 포함 3명이었어요. 개발팀과 협업할 일이 많았는데, HCL 파일을 코드 리뷰할 때 개발자들도 꽤 잘 읽더라고요. 선언적 문법이라 “이 리소스가 이렇게 생겼구나”를 직관적으로 파악하기 좋거든요.

    특히 이런 상황에서 Terraform이 강점을 발휘했어요:

    • 💡 팀에 인프라 전문가 비율이 높을 때 — HCL에 익숙해지면 오히려 더 깔끔하게 관리됨
    • 💡 레퍼런스가 중요할 때 — 거의 모든 클라우드 리소스에 대한 예제가 넘쳐남
    • 💡 안정성이 최우선일 때 — 10년 넘은 도구라 예측 가능한 동작이 보장됨
    • 💡 모듈 재사용이 핵심일 때 — Terraform Registry의 공개 모듈 생태계가 압도적
    # Terraform 모듈 활용 예시
    module "vpc" {
      source  = "terraform-aws-modules/vpc/aws"
      version = "~> 5.0"
    
      name = "my-vpc"
      cidr = "10.0.0.0/16"
    
      azs             = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
      private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
      public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
    
      enable_nat_gateway = true
    
      tags = {
        Environment = "production"
        Terraform   = "true"
      }
    }

    Terraform Registry에서 검증된 VPC 모듈 몇 줄로 완성이에요. 이런 거 보면 “역시 생태계가 갑이다” 싶죠.

    Terraform의 아픈 부분 😅

    근데 솔직히 말씀드리면, 복잡한 로직 처리할 때는 좀 힘들어요. 예를 들어 “조건에 따라 리소스 개수를 동적으로 결정”하려면…

    # Terraform에서 동적 리소스 생성 — 이게 직관적이지 않음
    resource "aws_security_group_rule" "ingress_rules" {
      count = length(var.ingress_ports)
    
      type              = "ingress"
      from_port         = var.ingress_ports[count.index]
      to_port           = var.ingress_ports[count.index]
      protocol          = "tcp"
      cidr_blocks       = ["0.0.0.0/0"]
      security_group_id = aws_security_group.main.id
    }
    
    # for_each를 쓰면 좀 낫지만, 여전히 복잡한 로직엔 한계가 있음
    resource "aws_instance" "servers" {
      for_each = var.server_configs
    
      ami           = each.value.ami
      instance_type = each.value.instance_type
    
      tags = {
        Name = each.key
      }
    }

    count, for_each 같은 메타 인수를 쓰다 보면 “이게 프로그래밍이 아니라 퍼즐 푸는 느낌”이 들 때가 있어요. 저도 처음엔 이게 뭔가 싶었는데, 익숙해지는 데 시간이 좀 걸렸습니다 ㅎㅎ

    Pulumi가 빛나는 순간들

    반면에 Pulumi는 개발자 팀에서 진가를 발휘하더라고요. 제가 사이드 프로젝트로 홈랩 인프라를 Pulumi(Python)로 관리해봤는데, 이런 점이 진짜 좋았어요:

    # Pulumi Python — 복잡한 로직도 그냥 Python으로
    import pulumi
    import pulumi_aws as aws
    
    # 환경별 설정을 딕셔너리로 관리
    env_configs = {
        "production": {"instance_type": "t3.medium", "count": 3},
        "staging":    {"instance_type": "t3.small",  "count": 1},
        "dev":        {"instance_type": "t3.micro",  "count": 1},
    }
    
    config = pulumi.Config()
    env = config.require("environment")
    current_config = env_configs[env]
    
    # 그냥 for 루프로 인스턴스 생성 — 이게 얼마나 자연스러운지!
    instances = []
    for i in range(current_config["count"]):
        instance = aws.ec2.Instance(
            f"web-server-{i}",
            ami="ami-0c55b159cbfafe1f0",
            instance_type=current_config["instance_type"],
            tags={
                "Name": f"web-server-{i}",
                "Environment": env,
            }
        )
        instances.append(instance)
    
    # 결과 출력도 Python 리스트 컴프리헨션으로
    pulumi.export("instance_ids", [inst.id for inst in instances])

    이거 처음 써봤을 때 “드디어 됐다!” 싶었어요. 프로그래밍 언어의 모든 기능을 그대로 쓸 수 있으니까, 복잡한 조건 처리나 반복 작업이 훨씬 자연스럽거든요.

    Pulumi가 특히 유리한 상황:

    • 💡 팀이 개발자 중심일 때 — TypeScript, Python 등 이미 아는 언어로 바로 시작 가능
    • 💡 인프라 로직이 복잡할 때 — 조건문, 반복문, 함수 등을 자유롭게 사용
    • 💡 테스트 자동화가 중요할 때 — pytest, Jest 등 기존 테스트 도구 그대로 활용
    • 💡 기존 코드베이스와 통합할 때 — 앱 코드와 인프라 코드를 같은 언어로 관리

    Pulumi의 아픈 부분 ⚠️

    근데 Pulumi도 완벽하진 않아요. 제가 겪은 몇 가지 불편한 점들:

    • 레퍼런스 부족 — 문제 생겼을 때 구글링해도 Terraform만큼 자료가 안 나와요. 특히 엣지 케이스는 직접 디버깅해야 하는 경우가 많았음
    • Pulumi Cloud 의존성 — 기본 상태 관리가 Pulumi Cloud를 통하는 방식이라, 자체 관리하려면 별도 설정이 필요
    • 언어별 지원 수준 차이 — TypeScript 지원이 가장 성숙하고, 다른 언어는 업데이트 시점이 조금씩 달라요
    • 팀 온보딩 — 인프라 엔지니어가 특정 프로그래밍 언어에 익숙하지 않으면 오히려 진입 장벽이 될 수 있음

    Pulumi Cloud 콘솔에서 스택 상태와 리소스 배포 결과를 확인하는 화면

    ⚠️ 실전에서 주의해야 할 것들

    Terraform 상태 파일(State File) 관리

    Terraform 쓰면서 가장 많이 삽질하는 부분이 상태 파일 관리예요. 처음에 로컬에 `terraform.tfstate` 파일 두고 팀에서 같이 쓰다가 충돌 났던 경험, 한 번쯤은 다들 있으실 거예요 ㅎㅎ

    # 반드시 원격 백엔드 설정하세요!
    terraform {
      backend "s3" {
        bucket         = "my-terraform-state"
        key            = "production/terraform.tfstate"
        region         = "ap-northeast-2"
        encrypt        = true
        dynamodb_table = "terraform-state-lock"  # 동시 수정 방지용 잠금
      }
    }
    

    DynamoDB로 상태 잠금(State Locking) 설정 안 해두면, 두 사람이 동시에 apply 실행할 때 상태 파일이 꼬일 수 있어요. 이거 한 번 겪어보면 절대 안 잊어버리게 됩니다 😅

    Pulumi 스택(Stack) 분리 전략

    Pulumi에서는 환경별로 스택을 분리하는 게 기본 패턴인데, 초반에 이걸 제대로 설계 안 하면 나중에 꽤 고생해요.

    # Pulumi 스택 생성 및 관리
    pulumi stack init production
    pulumi stack init staging
    pulumi stack init dev
    
    # 현재 스택 확인
    pulumi stack ls
    
    # 스택 전환
    pulumi stack select production
    
    # 배포 미리보기 (Terraform의 plan에 해당)
    pulumi preview
    
    # 실제 배포
    pulumi up

    💡 팁: Pulumi에서 환경별 설정값은 `pulumi config set`으로 관리하세요. 민감한 값은 `–secret` 플래그로 암호화해서 저장할 수 있어요.

    # 일반 설정값
    pulumi config set aws:region ap-northeast-2
    pulumi config set environment production
    
    # 민감한 정보는 암호화 저장
    pulumi config set --secret db_password "super-secret-password"

    Terraform 라이선스 변경 이후 고민

    앞서 언급했지만, 2023년 Terraform의 BSL 라이선스 전환은 꽤 큰 이슈였어요. 이 때문에 OpenTofu라는 오픈소스 포크가 Linux Foundation 산하에서 시작됐고, 많은 조직에서 마이그레이션을 고려하기 시작했죠. Pulumi로 넘어가는 팀들도 일부 있었고요.

    만약 상업적 사용이나 Terraform 기반 서비스 제공을 고려하고 있다면, BSL 1.1 조건을 법무팀과 함께 꼼꼼히 검토하시길 권장합니다.

    결국 뭘 선택해야 할까요? — 상황별 가이드

    제가 컨설팅이나 팀 내 논의에서 자주 받는 질문이 “그래서 뭐 써야 해요?”인데요. 정답은 없지만, 제 기준을 공유해 드릴게요.

    ✅ Terraform을 선택하면 좋은 경우

    • 팀에 인프라 전문가 비율이 높고, HCL에 거부감이 없을 때
    • 커뮤니티 레퍼런스와 안정성이 최우선일 때
    • Terraform Registry의 공개 모듈을 적극 활용하고 싶을 때
    • 기존 Terraform 코드베이스가 있는 팀에 합류할 때
    • 비교적 단순한 인프라 구성이 주를 이룰 때

    ✅ Pulumi를 선택하면 좋은 경우

    • 팀이 소프트웨어 개발자 중심이고, 특정 언어에 능숙할 때
    • 인프라 로직이 복잡해서 프로그래밍적 표현이 필요할 때
    • 인프라 코드에 유닛 테스트를 적용하고 싶을 때
    • 앱 코드와 인프라 코드를 같은 언어로 통합 관리하고 싶을 때
    • 라이선스 이슈에서 자유로운 완전 오픈소스 도구가 필요할 때

    팀 상황과 프로젝트 특성에 따른 Terraform vs Pulumi 선택 가이드 인포그래픽

    자주 묻는 질문 (FAQ)

    Q. Terraform에서 Pulumi로 마이그레이션할 수 있나요?

    네, Pulumi에서 공식적으로 변환 도구를 제공해요. `pulumi convert –from terraform` 명령으로 기본적인 리소스 변환이 가능합니다. 완벽하지는 않지만, 간단한 구성은 꽤 잘 변환되더라고요. 다만 복잡한 모듈이나 커스텀 프로바이더는 수동 작업이 필요할 수 있어요.

    Q. 둘 다 멀티 클라우드(Multi-Cloud)를 지원하나요?

    둘 다 AWS, Azure, GCP 등 주요 클라우드 프로바이더를 지원합니다. Terraform은 프로바이더 생태계가 더 방대하고, Pulumi는 Terraform 프로바이더를 브리지(Bridge)해서 쓸 수 있어서 커버리지 차이가 많이 줄어들었어요.

    Q. Pulumi는 무료인가요?

    Pulumi 자체는 오픈소스(Apache 2.0)로 무료입니다. Pulumi Cloud의 경우 개인 사용은 무료 플랜이 있고, 팀 협업 기능이나 고급 기능은 유료 플랜이 필요해요. 자체 백엔드(S3, Azure Blob Storage 등)를 사용하면 Cloud 없이도 운영 가능합니다.

    Q. 둘 다 Kubernetes(쿠버네티스) 관리에 적합한가요?

    네, 둘 다 Kubernetes 리소스 관리를 지원합니다. 다만 Kubernetes 전용으로는 Helm이나 Kustomize와의 조합이 더 일반적이에요. 클라우드 인프라(EKS 클러스터 생성 등)와 Kubernetes 리소스를 함께 관리할 때 IaC 도구가 유용하게 쓰입니다.

    마무리 — 결국 중요한 건 팀과 컨텍스트

    Pulumi vs Terraform 비교를 오래 해봤지만, 솔직히 말씀드리면 둘 다 훌륭한 IaC 도구입니다. 어느 쪽이 “객관적으로 더 좋다”라고 말하기가 어려워요.

    제 개인적인 포지션을 말씀드리면:

    • 🏢 엔터프라이즈 환경, 인프라 팀 주도 → Terraform (안정성, 레퍼런스, 생태계)
    • 🚀 스타트업, 개발자 팀, 복잡한 로직 → Pulumi (유연성, 언어 친숙도)
    • 🏠 홈랩, 개인 프로젝트 → 둘 다 배워보세요! 경험치 쌓기엔 최고

    저는 요즘 홈랩은 Pulumi(Python)로, 업무 환경은 Terraform으로 관리하고 있어요. 두 도구를 병행하면서 각각의 강점을 더 명확하게 느끼게 됐달까요.

    혹시 이미 둘 중 하나를 쓰고 계신 분들은, 반대편 도구도 간단한 사이드 프로젝트로 한번 써보시길 추천드려요. 비교해봐야 진짜 차이가 느껴지거든요.

    다음 글에서는 Terraform 상태 관리와 원격 백엔드 설정을 더 깊이 다뤄볼 예정이에요. Terraform 쓰다가 상태 파일로 삽질한 경험이 있으신 분들이라면 도움이 될 겁니다. 이전 글에서 다뤘던 클라우드 인프라 자동화 기초도 함께 참고해보세요!

    질문이나 의견은 댓글로 남겨주세요. 저도 아직 배우는 중이라, 여러분의 경험도 궁금합니다 😊

  • [k8s] ArgoCD 멀티 클러스터 GitOps 배포 및 관리 전략

    [k8s] ArgoCD 멀티 클러스터 GitOps 배포 및 관리 전략

    클러스터가 하나일 때는 괜찮았는데…

    처음 쿠버네티스를 도입했을 때 저도 클러스터 하나로 시작했어요. 개발(dev), 스테이징(staging), 프로덕션(production) 환경을 네임스페이스(Namespace)로 분리해서 쓰는 방식이었는데, 솔직히 그때는 그게 맞다고 생각했거든요. 근데 조직이 커지고 팀이 늘어나면서 문제가 하나씩 터지기 시작했습니다.

    “개발팀이 실수로 프로덕션 네임스페이스에 배포했어요.” 이 한 마디에 심장이 철렁 내려앉은 적 있으신가요? 저는 있습니다 ㅎㅎ. 결국 클러스터를 분리하기로 결정했고, 그때부터 ArgoCD 멀티 클러스터 전략을 본격적으로 파고들었습니다.

    이 글에서는 ArgoCD를 허브(Hub) 클러스터에 설치하고, 여러 개의 쿠버네티스 클러스터를 GitOps 방식으로 배포 및 관리하는 전략을 단계별로 정리해 드리겠습니다. 저처럼 삽질하지 않도록요.

    ArgoCD 멀티 클러스터 Hub-and-Spoke 아키텍처 다이어그램 - 허브 클러스터에서 dev, staging, prod 클러스터로 GitOps 배포 흐름

    ▲ ArgoCD 허브 클러스터가 여러 대상 클러스터(dev, staging, prod)를 관리하는 전체 아키텍처 구조. Hub-and-Spoke 패턴의 핵심입니다.

    ArgoCD 멀티 클러스터란? 개념부터 잡고 가요

    GitOps가 뭔지 먼저 짚고 가면

    GitOps는 쉽게 말해 “Git 저장소가 인프라와 애플리케이션의 단일 진실 공급원(Single Source of Truth)이 되는 운영 방식”이에요. 배포하고 싶으면 kubectl로 직접 명령을 날리는 게 아니라, Git에 커밋하면 자동으로 클러스터에 반영되는 방식이죠.

    ArgoCD는 이 GitOps를 쿠버네티스 위에서 구현해주는 대표적인 오픈소스 도구입니다. Git 저장소를 지속적으로 감시하다가 변경이 감지되면 클러스터에 자동으로 동기화(Sync)해줘요.

    멀티 클러스터 관리, 왜 필요한가요?

    네임스페이스 분리 방식의 가장 큰 문제는 폭발 반경(Blast Radius)이 너무 넓다는 거예요. 클러스터 레벨의 장애가 모든 환경에 영향을 주고, 보안 격리도 완벽하지 않습니다. 반면 클러스터를 분리하면:

    • 환경 간 완전한 격리 — 개발 배포 실수가 프로덕션에 영향 없음
    • 클러스터별 독립적인 리소스 할당 및 스케일링
    • 규정 준수(Compliance) 요구사항 충족 용이
    • 팀별 독립적인 업그레이드 주기 관리

    그런데 클러스터가 여러 개가 되면 관리 포인트도 여러 개가 되잖아요. 각 클러스터에 ArgoCD를 따로 설치하는 방법도 있지만, 저는 Hub-and-Spoke 패턴을 선호합니다. 허브 클러스터 하나에 ArgoCD를 설치하고, 나머지 클러스터들을 원격으로 관리하는 방식이에요.

    방식 장점 단점 추천 상황
    클러스터별 ArgoCD 설치 독립성 높음, 장애 격리 관리 포인트 분산, 일관성 유지 어려움 팀/조직이 완전히 분리된 경우
    Hub-and-Spoke (중앙화) 단일 관리 포인트, 일관된 정책 적용 허브 클러스터 장애 시 배포 불가 중앙 플랫폼팀이 관리하는 경우

    ArgoCD 설치 및 멀티 클러스터 등록 — 실전으로 들어갑니다

    1단계: 허브 클러스터에 ArgoCD 설치

    저는 허브 클러스터로 별도의 관리 전용 클러스터를 사용합니다. 프로덕션 워크로드가 없는 클러스터에 ArgoCD를 올리는 게 안전하더라고요.

    # ArgoCD 네임스페이스 생성
    kubectl create namespace argocd
    
    # ArgoCD 공식 매니페스트로 설치
    kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
    
    # 설치 확인
    kubectl get pods -n argocd
    

    설치가 완료되면 argocd-server 파드가 Running 상태가 되는데, 처음엔 이미지 풀(Image Pull) 때문에 좀 기다려야 할 수도 있어요. 저도 처음에 “왜 안 되지?” 하고 5분 기다렸더니 그냥 올라왔습니다 ㅎㅎ.

    # 초기 admin 비밀번호 확인
    kubectl -n argocd get secret argocd-initial-admin-secret \
      -o jsonpath="{.data.password}" | base64 -d
    
    # ArgoCD CLI 설치 (macOS 기준)
    brew install argocd
    
    # ArgoCD 서버 포트포워딩 (로컬 접근용)
    kubectl port-forward svc/argocd-server -n argocd 8080:443
    
    # CLI 로그인
    argocd login localhost:8080 --username admin --password <위에서_확인한_비밀번호> --insecure
    

    2단계: 대상 클러스터(Spoke) 등록

    이 부분이 멀티 클러스터 설정의 핵심이에요. ArgoCD CLI로 대상 클러스터를 등록하면, ArgoCD가 해당 클러스터에 argocd-manager라는 서비스 어카운트(Service Account)를 만들고 필요한 RBAC 권한을 자동으로 설정해줍니다.

    # 현재 kubeconfig에 등록된 컨텍스트 확인
    kubectl config get-contexts
    
    # 예시 출력:
    # CURRENT   NAME            CLUSTER         AUTHINFO
    # *         hub-cluster     hub-cluster     hub-admin
    #           dev-cluster     dev-cluster     dev-admin
    #           staging-cluster staging-cluster staging-admin
    #           prod-cluster    prod-cluster    prod-admin
    
    # 개발 클러스터 등록
    argocd cluster add dev-cluster --name dev
    
    # 스테이징 클러스터 등록
    argocd cluster add staging-cluster --name staging
    
    # 프로덕션 클러스터 등록
    argocd cluster add prod-cluster --name prod
    
    # 등록된 클러스터 목록 확인
    argocd cluster list
    

    💡 팁: 클러스터 등록 시 --name 옵션으로 별칭을 지정하면 나중에 Application 설정에서 훨씬 읽기 편합니다. URL 대신 이름으로 참조할 수 있거든요.

    ArgoCD Settings Clusters 화면에서 dev, staging, prod 멀티 클러스터가 등록된 관리 UI 화면

    ▲ ArgoCD 웹 UI의 Settings > Clusters 화면. dev, staging, prod 클러스터가 각각 등록되어 연결 상태를 실시간으로 확인할 수 있습니다.

    Git 저장소 구조 설계 — 이게 진짜 중요합니다

    멀티 클러스터 GitOps에서 Git 저장소 구조를 어떻게 잡느냐가 나중에 관리 편의성을 크게 좌우해요. 제가 여러 방식을 시도해보고 정착한 구조를 공유합니다.

    모노레포(Monorepo) 방식 — 제가 선호하는 방식

    gitops-repo/
    ├── apps/                          # 애플리케이션 정의
    │   ├── base/                      # 공통 기본 설정 (Kustomize base)
    │   │   ├── my-app/
    │   │   │   ├── deployment.yaml
    │   │   │   ├── service.yaml
    │   │   │   └── kustomization.yaml
    │   └── overlays/                  # 환경별 오버레이
    │       ├── dev/
    │       │   └── my-app/
    │       │       ├── kustomization.yaml
    │       │       └── patch-replicas.yaml
    │       ├── staging/
    │       │   └── my-app/
    │       └── prod/
    │           └── my-app/
    ├── argocd/                        # ArgoCD 설정 자체도 Git으로 관리
    │   ├── projects/                  # ArgoCD Project 정의
    │   │   ├── dev-project.yaml
    │   │   ├── staging-project.yaml
    │   │   └── prod-project.yaml
    │   └── applications/              # ArgoCD Application 정의
    │       ├── dev/
    │       ├── staging/
    │       └── prod/
    └── clusters/                      # 클러스터 레벨 설정
        ├── dev/
        ├── staging/
        └── prod/
    

    이 구조의 핵심은 Kustomize(커스터마이즈)를 활용해서 base 설정을 공유하면서 환경별로 다른 값(레플리카 수, 리소스 제한, 이미지 태그 등)만 오버레이로 덮어쓰는 방식이에요.

    Kustomize 오버레이 예시

    # apps/base/my-app/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: my-app
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: my-app
      template:
        metadata:
          labels:
            app: my-app
        spec:
          containers:
          - name: my-app
            image: my-registry/my-app:latest
            resources:
              requests:
                cpu: 100m
                memory: 128Mi
    
    # apps/overlays/prod/my-app/kustomization.yaml
    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    resources:
      - ../../../base/my-app
    patches:
      - path: patch-replicas.yaml
    images:
      - name: my-registry/my-app
        newTag: v1.2.3  # 프로덕션은 명시적 태그 사용
    
    # apps/overlays/prod/my-app/patch-replicas.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: my-app
    spec:
      replicas: 3  # 프로덕션은 3개로
    

    ArgoCD Application 및 AppProject 설정

    AppProject(앱 프로젝트)로 클러스터 접근 제어

    ArgoCD의 AppProject는 애플리케이션들을 논리적으로 그룹화하고, 어떤 Git 저장소에서 어떤 클러스터로 배포할 수 있는지 제한하는 역할을 합니다. 저는 이걸 환경별로 나눠서 씁니다.

    # argocd/projects/prod-project.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: AppProject
    metadata:
      name: production
      namespace: argocd
    spec:
      description: Production environment project
      # 허용된 소스 저장소
      sourceRepos:
        - 'https://github.com/myorg/gitops-repo.git'
      # 배포 가능한 대상 클러스터와 네임스페이스
      destinations:
        - server: https://prod-cluster-api.example.com
          namespace: '*'
      # 클러스터 범위 리소스 생성 제한 (선택적)
      clusterResourceWhitelist:
        - group: ''
          kind: Namespace
      # RBAC 역할 정의
      roles:
        - name: prod-deployer
          description: Production deployment role
          policies:
            - p, proj:production:prod-deployer, applications, sync, production/*, allow
          groups:
            - myorg:platform-team
    

    Application 정의 — 멀티 클러스터 배포의 핵심

    # argocd/applications/prod/my-app.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: my-app-prod
      namespace: argocd
      # 앱 삭제 시 리소스도 함께 삭제되지 않도록 (중요!)
      finalizers:
        - resources-finalizer.argocd.argoproj.io
    spec:
      project: production
      source:
        repoURL: https://github.com/myorg/gitops-repo.git
        targetRevision: main
        path: apps/overlays/prod/my-app  # 프로덕션 오버레이 경로
      destination:
        # 등록한 클러스터 이름 또는 API 서버 URL
        server: https://prod-cluster-api.example.com
        namespace: my-app
      syncPolicy:
        automated:
          prune: true       # Git에서 삭제된 리소스는 클러스터에서도 삭제
          selfHeal: true    # 클러스터 상태가 Git과 다르면 자동 복구
        syncOptions:
          - CreateNamespace=true  # 네임스페이스 없으면 자동 생성
          - PrunePropagationPolicy=foreground
        retry:
          limit: 5
          backoff:
            duration: 5s
            factor: 2
            maxDuration: 3m
    

    ApplicationSet으로 여러 클러스터에 한 번에 배포

    근데 여기서 더 편한 방법이 있어요. ApplicationSet(애플리케이션셋)을 쓰면 여러 클러스터에 대한 Application을 템플릿 하나로 자동 생성할 수 있거든요. 처음 이걸 알았을 때 “이게 왜 이렇게 편하지?” 싶었습니다.

    # argocd/applicationsets/my-app-all-clusters.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: ApplicationSet
    metadata:
      name: my-app-all-clusters
      namespace: argocd
    spec:
      generators:
        - list:
            elements:
              - cluster: dev
                url: https://dev-cluster-api.example.com
                env: dev
                revision: HEAD
              - cluster: staging
                url: https://staging-cluster-api.example.com
                env: staging
                revision: main
              - cluster: prod
                url: https://prod-cluster-api.example.com
                env: prod
                revision: main
      template:
        metadata:
          name: 'my-app-{{env}}'
        spec:
          project: '{{env}}'
          source:
            repoURL: https://github.com/myorg/gitops-repo.git
            targetRevision: '{{revision}}'
            path: 'apps/overlays/{{env}}/my-app'
          destination:
            server: '{{url}}'
            namespace: my-app
          syncPolicy:
            automated:
              prune: true
              selfHeal: true
            syncOptions:
              - CreateNamespace=true
    

    💡 팁: ApplicationSet의 generators에는 List 방식 외에도 클러스터 레이블 기반으로 자동 감지하는 Cluster Generator도 있어요. 클러스터가 많아질수록 이쪽이 훨씬 강력합니다.

    ⚠️ 실제로 겪은 트러블슈팅 — 이거 꼭 읽으세요

    문제 1: 클러스터 등록 후 연결 끊김 (Connection Refused)

    클러스터를 등록했는데 ArgoCD UI에서 계속 “Unknown” 상태가 뜨는 경우가 있었어요. 원인을 파고들어보니 허브 클러스터에서 대상 클러스터의 API 서버로 직접 네트워크 연결이 안 되는 거였습니다. VPC 피어링이나 방화벽 규칙을 확인해야 해요.

    # 허브 클러스터에서 대상 클러스터 API 서버 연결 테스트
    kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -- \
      curl -k https://prod-cluster-api.example.com/healthz
    

    문제 2: prune 옵션으로 인한 의도치 않은 리소스 삭제

    이건 진짜 아찔했던 경험인데요. automated.prune: true를 켜놨는데, 실수로 Git에서 파일을 지웠다가 프로덕션 Deployment가 통째로 삭제된 적이 있었어요. 다행히 빠르게 복구했지만…

    이후로 저는 프로덕션 환경에는 Sync Windows(동기화 창)를 설정해서 업무 시간 외에는 자동 동기화가 안 되도록 했습니다.

    # AppProject에 Sync Window 추가
    spec:
      syncWindows:
        - kind: allow
          schedule: '0 9 * * 1-5'  # 평일 오전 9시에만
          duration: 8h
          applications:
            - '*'
          manualSync: true  # 수동 동기화는 항상 허용
    

    문제 3: Helm 차트 버전 충돌

    여러 클러스터에 같은 Helm 차트를 다른 버전으로 배포하다 보면 values 파일 구조가 버전마다 달라서 오류가 나는 경우가 있어요. 저는 이걸 해결하기 위해 클러스터별 values 파일을 명확하게 분리해서 관리하고 있습니다.

    # Helm 소스 예시 - 클러스터별 values 파일 분리
    source:
      repoURL: https://charts.example.com
      chart: my-chart
      targetRevision: 1.2.3
      helm:
        valueFiles:
          - values.yaml
          - values-prod.yaml  # 환경별 values 파일
    
    ArgoCD Applications 대시보드에서 멀티 클러스터 배포된 애플리케이션들이 모두 Synced Healthy 상태로 표시된 결과 화면

    ▲ ArgoCD 웹 UI의 Applications 화면. dev, staging, prod 클러스터에 배포된 my-app들이 모두 Synced/Healthy 상태로 표시되는 모습입니다.

    ✅ 배포 검증 및 운영 팁

    헬스 체크와 동기화 상태 모니터링

    # 전체 Application 상태 확인
    argocd app list
    
    # 특정 앱 상세 상태 확인
    argocd app get my-app-prod
    
    # 수동으로 동기화 실행
    argocd app sync my-app-prod
    
    # 동기화 상태 및 히스토리 확인
    argocd app history my-app-prod
    
    # 롤백 (이전 버전으로)
    argocd app rollback my-app-prod 
    

    Notification(알림) 설정으로 배포 상태 파악

    ArgoCD에는 argocd-notifications라는 컴포넌트가 있어서 Slack, 이메일, PagerDuty 등으로 배포 상태를 알림 받을 수 있어요. 저는 Slack 채널에 연동해서 배포 성공/실패를 실시간으로 받고 있는데, 이거 없으면 이제 못 살 것 같습니다 ㅎㅎ.

    RBAC으로 팀별 접근 제어

    # argocd-rbac-cm ConfigMap 예시
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: argocd-rbac-cm
      namespace: argocd
    data:
      policy.csv: |
        # 개발팀: dev 프로젝트만 접근 가능
        p, role:dev-team, applications, *, dev/*, allow
        p, role:dev-team, applications, sync, dev/*, allow
        
        # 플랫폼팀: 전체 접근 가능
        p, role:platform-team, applications, *, */*, allow
        p, role:platform-team, clusters, *, *, allow
        
        g, myorg:dev-team, role:dev-team
        g, myorg:platform-team, role:platform-team
      policy.default: role:readonly
    

    전략 정리 — 이것만 기억하세요

    ArgoCD 멀티 클러스터 GitOps 전략 핵심 구성요소 요약 인포그래픽 - AppProject, ApplicationSet, Kustomize, RBAC 관계도

    ▲ ArgoCD 멀티 클러스터 GitOps 전략의 핵심 요소를 한눈에 정리한 요약 다이어그램. Git 저장소 구조, ArgoCD 컴포넌트, 클러스터 관계를 보여줍니다.

    구성 요소 역할 핵심 포인트
    AppProject 배포 범위 및 권한 제한 환경별로 분리, Sync Window 설정
    Application 개별 앱 배포 정의 destination.server로 대상 클러스터 지정
    ApplicationSet 다중 클러스터 배포 자동화 템플릿 하나로 여러 클러스터에 배포
    Kustomize Overlay 환경별 설정 분리 base 공유, 환경별 차이만 patch
    RBAC 팀별 접근 제어 AppProject role + argocd-rbac-cm

    마무리하며 — 그래서 뭐가 달라졌냐면

    ArgoCD 멀티 클러스터 구성을 제대로 잡고 나서 가장 크게 달라진 건, 배포에 대한 불안감이 사라졌다는 거예요. 예전엔 “내가 지금 어떤 클러스터에 붙어있지?”를 항상 확인해야 했는데, 이제는 Git에 PR 올리고 머지하면 끝이거든요.

    물론 처음 설계할 때 Git 저장소 구조를 잘 잡는 게 제일 중요합니다. 나중에 구조를 바꾸려면 진짜 손이 많이 가거든요. 저도 한 번 갈아엎었습니다… ㅎㅎ 이 글을 보시는 분들은 처음부터 overlays 구조로 시작하시길 강력 추천합니다.

    다음 글에서는 ArgoCD Image Updater를 활용해서 컨테이너 이미지 태그 업데이트까지 완전 자동화하는 방법을 다뤄볼 예정이에요. CI 파이프라인에서 이미지 빌드되면 ArgoCD가 자동으로 Git 커밋하고 배포까지 이어지는 풀 사이클인데, 이거 진짜 편합니다.

    혹시 멀티 클러스터 구성하면서 막히는 부분 있으시면 댓글로 남겨주세요. 제가 겪어본 삽질이라면 같이 해결해 드릴 수 있을 것 같습니다 🎉

    자주 묻는 질문 (FAQ)

    Q. ArgoCD 자체가 설치된 허브 클러스터가 다운되면 어떻게 되나요?

    A. 허브 클러스터가 다운되면 새로운 배포는 불가능하지만, 이미 배포된 애플리케이션들은 각 클러스터에서 계속 정상 동작합니다. 이 때문에 허브 클러스터는 고가용성(HA) 구성으로 운영하는 것을 권장합니다.

    Q. ApplicationSet과 Application을 언제 선택해야 하나요?

    A. 동일한 앱을 여러 클러스터에 배포해야 한다면 ApplicationSet이 훨씬 효율적입니다. 클러스터마다 배포 설정이 크게 다르거나 배포 시점을 완전히 독립적으로 관리해야 한다면 개별 Application을 사용하는 게 맞습니다.

    Q. Helm과 Kustomize 중 어떤 걸 쓰는 게 좋나요?

    A. 서드파티 차트를 그대로 쓸 때는 Helm, 자체 매니페스트를 환경별로 관리할 때는 Kustomize가 더 직관적입니다. 둘을 혼합해서 쓰는 것도 가능하고, 저는 실제로 두 방식을 같이 씁니다.