90DaysOfDevOps (Day 72)

고태규·2026년 1월 16일

DevOps

목록 보기
67/87
post-thumbnail

해당 스터디는 90DaysOfDevOps
https://github.com/MichaelCade/90DaysOfDevOps
를 기반으로 진행한 내용입니다.

Day 72 - Infrastructure as Code with Pulumi


1. 왜 Pulumi인가?


IaC는 이제 업계에서 널리 이해되고 있는 개념이다.

  • 확장성과 효율성: 반복적인 작업을 자동화함으로써 더 적은 인원으로 더 많은 일을 효율적으로 처리할 수 있게 한다.

  • 일관성: 다양한 플랫폼에 걸쳐 미리 정의된 클라우드 인프라 구성을 반복적이고 일관되게 배포할 수 있다.

  • 버전 관리 및 GitOps: Git과 같은 버전 관리 시스템과 결합하여 변경 이력과 주체를 추적할 수 있다. 또한 저장소에 커밋이 발생하면 파이프라인을 통해 인프라 변경을 자동 배포하는 GitOps 워크플로우 구현이 가능하다.

  • 협업: Pull Request, 코드 리뷰, 공유 저장소 등을 통해 팀 구성을 단순화하고 협업을 용이하게 한다.


그렇다면 다양한 IaC 툴중에서 왜 Pulumi를 쓰는가?

시중에는 Terraform(HCL 사용), CloudFormation(JSON/YAML 사용) 등 다양한 IaC 도구가 존재한다.

Pulumi는 이들과 달리 TypeScript, JavaScript, Go, Python과 같은 범용 프로그래밍 언어를 사용한다.

  • 기술의 재사용성: 독자적인 DSL(도메인 특화 언어)을 배우는 대신, Python이나 TypeScript 같은 언어 자체를 익히면 이는 IaC뿐만 아니라 다른 자동화나 개발 영역에서도 활용할 수 있는 자산이 된다.

  • 생태계 활용: Linter, 코드 분석 도구, IDE의 구문 강조 및 자동 완성 등 기존 프로그래밍 언어의 생태계를 그대로 활용할 수 있다.

  • Flow Control: if 문이나 for 루프와 같은 제어 구문을 직관적으로 사용할 수 있다. 이는 코드의 가독성을 높이고 유지보수를 용이하게 한다.

AWS CDK나 CDK for Terraform 같은 도구들은 코드를 작성하면 이를 CloudFormation이나 HCL 같은 중간 형식으로 컴파일한 뒤 API에 적용한다.

반면, Pulumi는 중간 형식을 사용하지 않는다.

Pulumi 엔진이 코드를 실행하여 리소스 그래프와 의존성을 생성하고, 공급자 플러그인을 통해 클라우드 API(AWS, Azure, Google 등)와 직접 통신한다.

참고사항: Terraform 코드를 Pulumi로 변환해주는 도구와, LLM 기반의 챗봇인 Pulumi AI도 제공되어 코드 작성을 돕는다.


2. Pulumi 구성요소


실습에 앞서 Pulumi에서 사용하는 주요 용어를 정리한다.

  • 프로젝트 (Project):

    • pulumi.yaml 프로젝트 파일과 프로그램 코드가 포함된 디렉토리를 의미
  • 스택 (Stack):

    • 인프라의 독립적인 인스턴스로, 고유의 설정 (Configuration)과 상태 (State)를 가진다.

    • 하나의 프로젝트는 반드시 최소 하나 이상의 스택을 가져야 한다.

  • 리소스 (Resources):

    • 클라우드 인프라를 구성하는 요소들이다. (예: Azure VNet, AWS VPC 등)
  • 입력과 출력:

    • 입력: 리소스 생성 시 전달하는 매개변수

    • 출력: 리소스 생성 후 클라우드 제공업체 API로부터 반환되는 속성
      (API 호출은 비동기적으로 이루어지므로, Pulumi는 출력값이 해결될 때까지 기다렸다가 이를 의존성이 있는 다른 리소스의 입력으로 사용)


3. 실습: Pulumi 프로젝트 생성 및 S3 버킷 배포


해당 프레젠테이션 실습에서는 Pulumi CLI를 사용하여 AWS TypeScript 프로젝트를 초기화하고, 기본 예제 코드로 AWS S3 버킷을 생성하는 과정을 다룬다.

3-1. 사전 준비

실습을 진행하기 위해 로컬 환경에 다음 도구들이 설치되어 있어야 한다.

  • Pulumi CLI: 설치 후 pulumi login 완료 상태

  • Node.js & npm: TypeScript 실행 런타임

  • AWS CLI: aws configure를 통해 자격 증명(Access Key) 설정 완료 상태

3-2. 프로젝트 디렉토리 생성 및 초기화

터미널을 열고 프로젝트를 진행할 디렉터리를 생성한 뒤, pulumi new 명령어를 통해 AWS TypeScript 템플릿을 다운로드한다.

# Pulumi 프로젝트 초기화 (AWS TypeScript 템플릿 사용)
# 이 명령어를 실행하면 대화형 프롬프트가 실행된다.
pulumi new aws-typescript

명령어 실행 시 나타나는 프롬프트에는 다음과 같이 입력한다.

  • project name: (엔터 눌러서 기본값 사용)

  • project description: a demo program (또는 자유롭게 입력)

  • stack name: dev (엔터 눌러서 기본값 사용)

  • aws:region: us-west-2 (또는 본인이 사용하는 리전 입력, 예: ap-northeast-2)

입력이 완료되면 npm install이 자동으로 실행되며 필요한 의존성 패키지를 설치한다.

3-3. 코드 분석 (index.ts)

설치가 완료되면 index.ts 파일이 생성된다.

이는 Pulumi 프로그램의 진입점이다. 기본 템플릿은 S3 버킷 하나를 생성하는 코드를 포함하고 있다.

// index.ts 파일 내용
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

// 1. AWS S3 버킷 리소스 생성
// "my-bucket"은 Pulumi가 관리하는 논리적 이름
const bucket = new aws.s3.Bucket("my-bucket");

// 2. 스택 출력 정의
// 생성된 버킷의 실제 ID(이름)를 프로그램 외부로 내보냄
export const bucketName = bucket.id;

3-4. 인프라 배포

코드가 준비되었으므로 실제 AWS 클라우드에 리소스를 배포한다.

pulumi up 명령어는 현재 코드 상태와 클라우드 상태를 비교하여 변경 사항을 적용한다.

# 인프라 생성 및 업데이트 실행
pulumi up

명령어를 실행하면 Preview(미리보기) 화면이 출력된다.

  • Type: aws:s3:Bucket

  • Plan: create (생성 예정)

내용을 확인한 후 키보드 방향키로 yes를 선택하고 엔터를 누르면 실제 배포가 진행된다.

3-5. 리소스 삭제

실습이 끝난 후 불필요한 과금을 방지하기 위해 생성된 리소스를 삭제한다.

# 생성된 모든 리소스 제거
pulumi destroy

4. 상태 관리 및 백엔드


모든 IaC 도구는 선언적 작업을 수행하기 위해 상태(State)를 저장해야 한다. 그래야 현재 상태와 목표 상태를 비교할 수 있다.

  • Pulumi Cloud (SaaS):

    • 기본 백엔드이며, 상태와 시크릿을 관리해준다.

    • 평생 무료 개인 티어를 제공하여 개인 프로젝트에 적합하다. (app.pulumi.com)

  • 기타 백엔드:

    • S3, Azure Blob Storage, GCS 같은 클라우드 스토리지나 로컬 파일시스템(PoC 용도)도 지원한다.
  • 로그인:

    • pulumi login 명령어를 통해 백엔드에 인증해야 한다. pulumi whoami -v 명령어로 현재 로그인된 백엔드 정보를 확인할 수 있다.


Pulumi Cloud의 평생 무료 개인 티어?

타 IaC 도구의 SaaS들이 무료 플랜에서 리소스 개수나 배포 횟수에 제한을 두는 것과 달리, Pulumi는 개인 사용자에게 엔터프라이즈급의 확장성을 제공한다.

  1. 핵심 제공 스펙

    • 사용자 수 1명: 가장 큰 제약 사항으로, 팀원과 계정을 공유하거나 협업 기능을 사용할 수 없다.

    • 관리 리소스 수 무제한: AWS, Azure 등의 리소스를 수백, 수천 개 생성하여 관리해도 추가 비용이 발생하지 않는다. 학습용이나 홈랩 구축 시 매우 강력한 장점이다.

    • 스택(Stack) 수 무제한: dev, prod, staging, test-feature 등 원하는 만큼 스택을 나누어 독립적으로 관리할 수 있다.

    • 배포 횟수 및 기록 무제한: 하루에 수십 번 배포(pulumi up)를 실행해도 과금되지 않으며, 과거 배포 이력을 조회하거나 특정 시점으로 상태를 롤백하는 기능도 제한 없이 사용 가능하다.

2) 보안 및 관리 기능

  • 상태(State) 호스팅: S3 버킷 등을 따로 구축할 필요 없이 Pulumi Cloud가 안전하게 상태 파일을 저장 및 관리한다.

  • Secrets 관리: API 키나 비밀번호와 같은 민감 정보는 자동으로 암호화되어 저장되며, 코드 상에서는 평문으로 노출되지 않는다.

  • 계정 보안: MFA(다중 요소 인증)를 지원하여 개인 계정의 보안을 강화할 수 있다.


Terraform과 차이는?

Terraform도 Pulumi Cloud와 똑같은 역할을 하는 관리형 서비스인 HCP Terraform (구 Terraform Cloud)이 있는데, 이 둘의 무료 정책에는 차이가 존재한다.

  • 방식: Terraform CLI를 깔고, 상태 파일(tfstate)을 내 AWS S3 버킷에 저장

  • 비용: 사실상 0원 (S3 스토리지 비용 몇십 원 수준).

  • 제한: 리소스 개수, 사용자 수 제한 없으며, 무제한

  • 단점 (Pulumi Cloud 무료 티어와 비교 시):

    • UI가 없음: Pulumi Cloud처럼 웹에서 그래프로 보고, 클릭해서 배포 기록을 보는 대시보드가 없으므로, 터미널에서만 확인해야함.

    • Locking 설정 번거로움: 협업 시 동시에 수정하는 걸 막으려면 DynamoDB 등을 따로 연결해서 Lock 설정을 직접 해줘야함.

    • 시크릿 관리: API 키 같은 비밀번호 관리를 스스로 암호화 처리해야 함.


Pulumi Cloud와 HCP Terraform 차이

비교 항목Pulumi Cloud (개인 무료)HCP Terraform (무료 티어)
핵심 제한사용자 1명 (협업 불가)리소스 500개 (협업 가능)
리소스 수무제한500개까지만 무료
사용자 수1명 (혼자만 가능)500개까지만 무료
특징혼자 공부할 때 스케일이 커져도 걱정 없음팀 프로젝트엔 좋지만, 인프라 커지면 유료 전환 압박

5. 실습 2 : 실제 인프라 구축 코드 분석


단순한 S3 버킷 생성을 넘어, 실제 운영 환경과 유사하게 네트워크(VPC)를 구성하고 서버(EC2)를 띄운 뒤, 그 내부에서 애플리케이션(Docker)을 실행하는 심화 실습이다.

해당 과정에서 Pulumi의 기능인 비동기 출력 처리와 명시적 의존성 관리 방법을 확인할 수 있다.


5-1. 코드 주요 흐름 (index.ts)

발표자가 설명한 로직을 바탕으로 재구성한 index.ts의 핵심 코드는 다음과 같다.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as docker from "@pulumi/docker";
import * as time from "@pulumiverse/time"; 

// 1. 매개변수 정의 (Config)
// 사용자로부터 설정값을 받아 재사용성 확보
const config = new pulumi.Config();
const instanceType = config.get("instanceType") || "t3.micro";
const networkCidr = config.get("networkCidr") || "10.0.0.0/16";

// 2. VPC 및 네트워크 리소스 생성
const vpc = new aws.ec2.Vpc("demo-vpc", {
    cidrBlock: networkCidr,
    enableDnsHostnames: true,
});

const subnet = new aws.ec2.Subnet("demo-subnet", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    mapPublicIpOnLaunch: true, // 퍼블릭 IP 자동 할당
});

const igw = new aws.ec2.InternetGateway("demo-igw", { vpcId: vpc.id });
// (라우트 테이블 및 연결 설정 코드는 생략됨)

// 3. AMI ID 조회 (비동기 처리)
// 'getAmi'는 클라우드 API를 호출해야 하므로 즉시 값을 알 수 없다.
// Pulumi는 이 값이 해결될 때까지 기다렸다가 인스턴스 생성에 사용한다.
const ami = aws.ec2.getAmi({
    filters: [{ name: "name", values: ["flatcar-stable-*"] }], // Flatcar Linux 사용
    owners: ["..."], // Flatcar 소유자 ID
    mostRecent: true,
});

// 4. 보안 그룹 (Security Group) 생성
const sg = new aws.ec2.SecurityGroup("ssh-allow", {
    vpcId: vpc.id,
    ingress: [{ protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] }],
    egress: [{ protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] }],
});

// 5. EC2 인스턴스 생성
// 위에서 생성된 VPC, Subnet, SG의 ID(Output)를 입력(Input)으로 사용한다.
const server = new aws.ec2.Instance("demo-bastion", {
    instanceType: instanceType,
    vpcSecurityGroupIds: [sg.id],
    ami: ami.then(a => a.id), // Promise가 해결된 후 ID 사용
    subnetId: subnet.id,
    keyName: "my-key-pair",
});

// 6. 명시적 의존성 및 지연 처리 (Boot Delay)
// EC2가 생성되어도 SSH 데몬이 뜰 때까지 시간이 걸린다.
// Docker 프로바이더가 접속 실패하는 것을 막기 위해 30초 대기를 강제한다.
const bootDelay = new time.Sleep("boot-delay", {
    createDuration: "30s",
}, { dependsOn: [server] }); // server가 생성된 후 실행되도록 의존성 설정

// 7. Docker 배포 
// EC2 인스턴스 내부에서 Docker 컨테이너를 실행한다.
// bootDelay가 끝난 후에 실행되도록 'dependsOn'을 설정한다.
const nginxContainer = new docker.Container("nginx", {
    image: "nginx:latest",
    ports: [{ internal: 80, external: 80 }],
}, { 
    provider: new docker.Provider("remote-docker", {
        host: server.publicIp.apply(ip => `ssh://core@${ip}`), // SSH로 접속
    }),
    dependsOn: [bootDelay] // 30초 대기가 끝난 후 실행
});
  • Inputs & Outputs: server 리소스를 만들 때 subnet.id, sg.id와 같은 Output 값을 사용했다. Pulumi는 이를 감지하고 "Subnet과 SG가 먼저 만들어져야 서버를 만들 수 있다"는 의존성 그래프를 자동으로 그린다.

  • dependsOn (명시적 의존성): bootDelaynginxContainer는 코드 상으로는 서로 연관 없는 객체처럼 보일 수 있다. 이때 dependsOn 옵션을 사용하여 **"서버 부팅 -> 30초 대기 -> 컨테이너 실행"**이라는 순서를 강제로 지정해주었다.

5-2. 실행 (pulumi up)

작성한 코드를 배포한다.

pulumi up -y
  • 실행 결과: 약 3~4분의 시간이 소요된다.

  • 리소스 생성: 터미널 로그를 확인하면 VPC, Subnet, Internet Gateway, Route Table, EC2 Instance, 그리고 Docker Container까지 총 36개의 리소스가 생성되는 것을 볼 수 있다.

  • 특이 사항: EC2 인스턴스가 생성된 후 boot-delay 단계에서 잠시 멈춰 있는 것을 볼 수 있는데, 이는 설정한 30초 대기 로직이 정상 작동하는 것이다.

5-3. 스택 출력 활용

인프라가 배포되었지만, 생성된 EC2의 IP 주소가 무엇인지 터미널에는 바로 표시되지 않는다.

프로그램 내부의 값(IP)을 외부로 노출하기 위해 Stack Output을 사용한다.

// ... index.ts 파일 코드 하단 ...

// 생성된 EC2 인스턴스의 퍼블릭 IP를 'instancePubIP'라는 이름으로 내보낸다.
export const instancePubIP = server.publicIp;

Stack Output을 사용하기 위하여 index.ts 파일 코드 하단에 해당 코드를 추가하여 pulumi up 명령어를 사용하여 업데이트를 실행하면, IP를 외부로 노출할 수 있다.

또한, 이미 EC2와 네트워크는 생성되어 있고 변경된 설정이 없으므로, 인프라 리소스는 건드리지 않고오직 Outputs 설정만 업데이트한다. 따라서 배포 속도가 매우 빠르다.

이후, 터미널 명령어로 IP를 조회할 수 있다.

# 1. IP 주소 확인
pulumi stack output instancePubIP
> 54.123.45.67  

# 2. 검증 (Docker 컨테이너 확인)
ssh core@$(pulumi stack output instancePubIP)
docker container ls

6. 정리


Pulumi는 별도의 DSL 학습 없이 Python, TypeScript 등 범용 프로그래밍 언어로 인프라를 정의할 수 있는 강력한 IaC 도구이다.

중간 컴파일 과정 없이 클라우드 API와 직접 통신하며, 익숙한 코드의 제어 구문(조건문, 반복문)을 활용해 유연하고 복잡한 아키텍처를 손쉽게 구성할 수 있다.

무엇보다 Pulumi Cloud의 개인 무료 티어는 리소스 수에 제한이 없어, 비용 걱정 없이 대규모 학습이나 토이 프로젝트를 진행하기에 최적화되어 있다.

기존 개발 언어의 생태계(IDE, 테스트 도구 등)를 그대로 활용하면서 진정한 의미의 Code로 인프라를 관리하고 싶다면 Pulumi는 최고의 선택지가 될 것이다.


0개의 댓글