본문으로 건너뛰기
김신건의 로그

[IaC] Pulumi: 코드로 인프라 (TS/Python/Go)

· 수정 · 📖 약 1분 · 502자/단어 #pulumi #iac #devops #cloud
Pulumi, IaC in code, Pulumi stack, Pulumi automation API, Pulumi ESC, CrossGuard

정의

Pulumi = 일반 프로그래밍 언어로 IaC. TypeScript, Python, Go, .NET, Java. Terraform 의 HCL DSL 대신 코드 자유도.

사용 상황

상황이유
반복/조건 분기가 많은 인프라언어 자유도 활용
내부 플랫폼 도구 (self-service portal)Automation API
멀티 스택, 환경 분리Stack + ESC
기존 Terraform 마이그레이션import + 호환 provider
인프라에 unit test 적용Pulumi testing SDK
Policy as CodeCrossGuard

TS 예시

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config();
const env = pulumi.getStack();

const bucket = new aws.s3.Bucket("data", {
  bucket: `myapp-data-${env}`,
  tags: { Environment: env, ManagedBy: "pulumi" },
});

new aws.s3.BucketVersioningV2("data-version", {
  bucket: bucket.id,
  versioningConfiguration: { status: "Enabled" },
});

export const bucketName = bucket.id;

흐름

flowchart LR
    Code["index.ts"] --> Preview["pulumi preview (diff)"]
    Preview --> Up["pulumi up (실제 변경)"]
    Up --> State[("stack state<br/>backend")]
    State --> Drift["drift detection"]

명령

pulumi new aws-typescript    # 새 프로젝트
pulumi stack init dev        # 새 stack (Terraform workspace 비슷)
pulumi config set region us-east-1
pulumi config set --secret db-password xxx
pulumi preview               # diff
pulumi up                    # apply
pulumi destroy
pulumi stack output bucketName
pulumi import aws:s3/bucket:Bucket data my-legacy-bucket

Backend (state)

Backend의미
Pulumi Cloudmanaged (기본, free tier)
AWS S3self-host
Azure Blob / GCSself-host
Local file개발
pulumi login s3://my-pulumi-state
pulumi login --local

차이: HCL vs 코드

// Pulumi: 조건/반복은 코드 그대로
const instances = ["alice", "bob", "charlie"].map((name) =>
  new aws.iam.User(name, { name })
);

// 의존성 자동
const role = new aws.iam.Role("app", { assumeRolePolicy: "..." });
new aws.iam.RolePolicyAttachment("attach", {
  role: role.name,    // pulumi.Output<string>, 의존성 자동
  policyArn: policy.arn,
});

Pulumi vs Terraform

항목PulumiTerraform
언어TS/Python/Go/.NET/JavaHCL
표현력높음 (코드 자유)DSL 한정
학습 곡선일반 코드 알면 쉬움HCL 학습
테스트unit test 가능tflint, terratest 등 별도
생태계작음가장 큼
가격Cloud paid (free tier)OSS / OpenTofu
멀티 클라우드

Stack 관리

Stack = 환경 단위 (dev, staging, prod). 각 stack은 독립적 state.

pulumi stack init staging
pulumi stack select dev
pulumi stack ls
pulumi stack rm old-stack --force

Stack 별 config

const config = new pulumi.Config();
const region = config.require("region");       // 없으면 에러
const debug = config.getBoolean("debug") ?? false;
# dev 스택
pulumi config set region ap-northeast-2 --stack dev

# prod 스택
pulumi config set region us-east-1 --stack prod

Stack References (cross-stack output)

// 네트워크 스택에서 VPC ID 읽기
const netStack = new pulumi.StackReference("org/network/prod");
const vpcId = netStack.getOutput("vpcId");

const cluster = new aws.ecs.Cluster("app", {
  tags: { VpcId: vpcId.apply(id => String(id)) },
});

스택 간 의존성을 명시적으로 표현. Terraform data.terraform_remote_state 와 동일 패턴.

Component Resources

재사용 가능한 고수준 추상화.

class SecureBucket extends pulumi.ComponentResource {
  public readonly bucket: aws.s3.Bucket;
  public readonly bucketName: pulumi.Output<string>;

  constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
    super("myapp:index:SecureBucket", name, {}, opts);

    this.bucket = new aws.s3.Bucket(name, {
      acl: "private",
      serverSideEncryptionConfiguration: {
        rule: {
          applyServerSideEncryptionByDefault: {
            sseAlgorithm: "AES256",
          },
        },
      },
    }, { parent: this });

    this.bucketName = this.bucket.id;
    this.registerOutputs({ bucketName: this.bucketName });
  }
}

// 사용
const dataBucket = new SecureBucket("data");

CrossGuard (Policy as Code)

인프라 정책을 코드로. 배포 전 자동 검사.

// policy-pack/index.ts
import { PolicyPack, validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";

new PolicyPack("aws-security", {
  policies: [
    {
      name: "s3-no-public-read",
      description: "S3 버킷 public read 금지",
      enforcementLevel: "mandatory",
      validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, report) => {
        if (bucket.acl === "public-read") {
          report("public-read ACL 은 금지입니다.");
        }
      }),
    },
  ],
});
pulumi up --policy-pack ./policy-pack

단위 테스트

// __tests__/infra.test.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

pulumi.runtime.setMocks({
  newResource: (type, name, inputs) => ({ id: `${name}-id`, state: inputs }),
  call: (token, args, provider) => ({ result: args }),
});

import { SecureBucket } from "../src/secure-bucket";

test("S3 bucket 암호화 활성화", async () => {
  const bucket = new SecureBucket("test");
  const sse = await bucket.bucket.serverSideEncryptionConfiguration.get();
  expect(sse?.rule?.applyServerSideEncryptionByDefault?.sseAlgorithm).toBe("AES256");
});
npx vitest run     # 또는 jest

Automation API

import * as automation from "@pulumi/pulumi/automation";

const stack = await automation.LocalWorkspace.createOrSelectStack({
  stackName: "prod",
  projectName: "myapp",
  program: async () => {
    new aws.s3.Bucket("data", { acl: "private" });
  },
});

await stack.up({ onOutput: console.log });

Terraform 의 CLI 의존성 없이 Pulumi 를 프로그램 안에서 호출. self-service portal 같은 내부 도구 에 강력.

Pulumi ESC (Environments, Secrets, Configuration)

환경별 비밀, 설정을 중앙 관리. OIDC로 동적 credentials 발급.

# esc-env.yaml
values:
  aws:
    login:
      fn::open::aws-login:
        oidc:
          duration: 1h
          roleArn: arn:aws:iam::123:role/pulumi-esc
          sessionName: pulumi-esc

  environmentVariables:
    AWS_ACCESS_KEY_ID: ${aws.login.accessKeyId}
    AWS_SECRET_ACCESS_KEY: ${aws.login.secretAccessKey}
    AWS_SESSION_TOKEN: ${aws.login.sessionToken}
esc env open myorg/prod
esc run myorg/prod -- pulumi up

GitHub Actions CI/CD

name: Pulumi Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # OIDC
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: pulumi/actions@v5
        with:
          command: up
          stack-name: prod
          cloud-url: s3://my-pulumi-state
        env:
          PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
          AWS_REGION: ap-northeast-2

흔한 함정

WARNING

  1. pulumi.Output 처리 미숙 = .apply(), interpolate 사용. promise/await 와 다름.
  2. Secret 코드에 평문 = pulumi config set --secret 사용.
  3. Stack 간 reference = StackReference 로 cross-stack output 읽기.
  4. 변수 vs config = config 가 stack 별. 변수는 코드.
  5. ComponentResource 에서 parent: this 누락 = state 에 고아 리소스. 항상 { parent: this } 전달.
  6. pulumi destroy 순서 = 의존 stack 먼저 destroy. StackReference 있으면 역순.
  7. drift 탐지 = pulumi refresh 로 실제 인프라 상태와 state 동기화.

관련 위키

이 글의 용어 (6개)
[AWS] CDK (Cloud Development Kit)cloud
정의 AWS CDK (Cloud Development Kit) 는 프로그래밍 언어 (TypeScript, Python, Java, C#, Go) 로 AWS 인프라를 정의하여 Cl…
[AWS] CloudFormationcloud
정의 AWS CloudFormation 은 JSON 또는 YAML 템플릿으로 AWS 인프라를 선언적으로 프로비저닝하는 IaC (Infrastructure as Code) 서비스입…
[CI/CD] GitHub Actions: workflow, action, runnerdevops
정의 GitHub Actions = GitHub 내장 CI/CD. YAML workflow + marketplace action. 2018 출시 → Travis CI 대체. 구조…
[GitOps] 패턴: 단일 vs 다중 repo, environment promotiondevops
정의 GitOps = Git 을 single source of truth 로 삼아 자동으로 인프라 + 앱 상태를 reconcile 하는 운영 방식. GitOps 4가지 원칙 (O…
[GitOps] ArgoCD: Kubernetes GitOpsdevops
정의 ArgoCD = Kubernetes 의 GitOps 컨트롤러. Git 리포지토리의 manifest 가 source of truth → cluster 가 자동 동기화. Git…
[IaC] Terraform: HCL, provider, statecloud
정의 Terraform = 선언적 인프라 코드. HCL (HashiCorp Configuration Language) 로 리소스 정의 → AWS/GCP/Azure 등 provid…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기