Pulumi 인프라를 코드로은(는) 무엇을 하나요?
빠른 가이드: 완전한 타입 안전성을 갖춘 TypeScript로 클라우드 인프라를 정의하세요.
ComponentResource를 사용하여 재사용 가능한 인프라 패턴을 캡슐화하세요. 컴포넌트 내부의 모든 자식 리소스에{ parent: this }를 전달하세요. Output을 사용한 문자열 빌드에는pulumi.interpolate를 사용하고, 문자열 연결은 사용하지 마세요..apply()내부에서 리소스를 생성하지 마세요. 민감한 값에는Config.requireSecret()을 사용하세요. 비권장transformations보다transforms를 선호하세요. 컴포넌트 생성자 마지막에 항상this.registerOutputs()를 호출하세요.
<critical_requirements>
중요: 이 스킬 사용 전 필수 사항
모든 코드는 CLAUDE.md의 프로젝트 규칙을 따라야 합니다 (kebab-case, 명명된 내보내기, import 순서,
import type, 명명된 상수)
(ComponentResource 내부의 모든 자식 리소스에 { parent: this }를 전달해야 합니다. 생략하면 리소스 트리와 상태 추적이 손상됩니다.)
(Output을 사용한 문자열 빌드에는 반드시 pulumi.interpolate를 사용해야 합니다. 문자열 연결은 자동으로 [object Object]를 생성합니다.)
(.apply() 내부에서 절대 리소스를 생성하지 마세요. pulumi preview에 나타나지 않으며 순서 문제가 발생합니다.)
(민감한 값에는 반드시 Config.requireSecret()을 사용하세요. Config.require()는 값을 일반 텍스트로 상태에 저장합니다.)
(모든 ComponentResource 생성자 마지막에 this.registerOutputs()를 호출해야 합니다. 생략하면 출력 추적이 불가능합니다.)
</critical_requirements>
상세 리소스:
- examples/core.md - 리소스 정의, 컴포넌트 리소스, 명명, Outputs, 구성/비밀
- examples/advanced.md - 스택 참조, 변환, 동적 공급자, Automation API, 정책 팩
- reference.md - 의사 결정 프레임워크, 리소스 옵션 테이블, API 참조, CLI 명령어
자동 감지: Pulumi, @pulumi/pulumi, @pulumi/aws, @pulumi/gcp, @pulumi/azure, @pulumi/kubernetes, pulumi.ComponentResource, pulumi.CustomResource, pulumi.Output, pulumi.Config, pulumi.interpolate, pulumi.all, registerOutputs, StackReference, ComponentResourceOptions, CustomResourceOptions, dynamic.Resource, dynamic.ResourceProvider, LocalWorkspace, InlineProgramArgs, Automation API, CrossGuard, PolicyPack
사용 시기:
- 타입 안전한 리소스 API로 TypeScript에서 클라우드 인프라 정의하기
ComponentResource로 재사용 가능한 인프라 컴포넌트 생성하기- 스택 참조로 멀티 스택 아키텍처 관리하기
- 비밀 및 환경별 구성 처리하기
- Automation API로 셀프 서비스 인프라 플랫폼 구축하기
- CrossGuard 정책 팩으로 규정 준수 정책 작성하기
사용하지 말아야 할 경우:
- 단일 리소스를 생성하는 일회성 셸 스크립트 (클라우드 CLI를 직접 사용하세요)
- 팀에 TypeScript 경험이 없는 프로젝트 (다른 IaC 언어 옵션을 고려하세요)
주요 패턴:
- 타입 입력 및 자동 명명을 사용한 리소스 정의
- ComponentResource 캡슐화 (parent, 명명, registerOutputs)
- Outputs:
apply,all,interpolate및 리프팅 - 구성 및 비밀 관리 (
Config.require,Config.requireSecret,pulumi.secret) - 스택 참조를 통한 교차 스택 데이터 공유
- 리소스 옵션 (
dependsOn,protect,aliases,ignoreChanges,transforms) - 사용자 정의 CRUD 리소스를 위한 동적 공급자
- 프로그래밍 방식 스택 관리를 위한 Automation API
- 규정 준수 시행을 위한 CrossGuard 정책 팩
철학
Pulumi는 인프라를 구성 파일이 아닌 실제 코드로 취급합니다. TypeScript는 타입 안전성, IDE 자동 완성, 리팩토링 도구 및 전체 Node.js 생태계를 제공합니다. 리소스는 객체이고, 종속성은 자동이며, 재사용은 사용자 정의 모듈 언어가 아닌 함수와 클래스를 통해 이루어집니다.
핵심 원칙:
- 리소스는 객체입니다: 모든 클라우드 리소스는 타입화된 입력과 출력을 가진 TypeScript 클래스 인스턴스입니다.
- 종속성은 자동입니다: 한 리소스의 출력을 다른 리소스의 입력으로 전달하면 Pulumi가 종속성 그래프를 추론합니다.
- 컴포넌트를 통한 재사용:
ComponentResource는 여러 리소스를 자체 입력과 출력을 가진 단일 논리 단위로 캡슐화합니다. - 출력은 프로미스입니다:
Output<T>는 배포 후까지 알 수 없는 값을 나타냅니다. 이를 다룰 때는apply,all또는interpolate를 사용하고, 수동으로 언래핑하지 마세요. - 상태가 관리됩니다: Pulumi는 모든 리소스를 상태에서 추적합니다. 논리적 이름을 변경하거나 파일 간에 리소스를 이동하면
aliases를 사용하지 않는 한 삭제 후 다시 생성됩니다.
핵심 패턴
패턴 1: 리소스 정의
모든 리소스는 논리적 이름(상태 추적에 사용됨), args 모음(타입화된 입력) 및 선택적 리소스 옵션을 사용합니다.
const bucket = new aws.s3.Bucket(
"data-bucket",
{
versioning: { enabled: true },
lifecycleRules: [
{ enabled: true, expiration: { days: BUCKET_EXPIRY_DAYS } },
],
},
{ protect: true },
); // 실수로 인한 삭제 방지
핵심 사항: 논리적 이름은 스택 내에서 유형별로 고유해야 합니다. Pulumi는 충돌을 방지하기 위해 물리적 이름에 자동으로 무작위 접미사를 추가합니다. 중요한 리소스에는 protect: true를 사용하세요. 교차 스택 소비를 위해 출력을 내보내세요.
리소스 명명, 자동 명명 구성 및 공급자 옵션에 대한 자세한 내용은 examples/core.md를 참조하세요.
패턴 2: ComponentResource 캡슐화
관련 리소스를 ComponentResource로 래핑하여 재사용 가능한 인프라 단위를 만드세요.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface StaticSiteArgs {
indexDocument?: string;
errorDocument?: string;
}
class StaticSite extends pulumi.ComponentResource {
public readonly bucketName: pulumi.Output<string>;
public readonly websiteUrl: pulumi.Output<string>;
constructor(
name: string,
args: StaticSiteArgs,
opts?: pulumi.ComponentResourceOptions,
) {
super("myinfra:web:StaticSite", name, args, opts);
const bucket = new aws.s3.BucketV2(`${name}-bucket`, {}, { parent: this });
const website = new aws.s3.BucketWebsiteConfigurationV2(
`${name}-website`,
{
bucket: bucket.id,
indexDocument: { suffix: args.indexDocument ?? "index.html" },
errorDocument: { key: args.errorDocument ?? "error.html" },
},
{ parent: this },
);
this.bucketName = bucket.id;
this.websiteUrl = website.websiteEndpoint;
this.registerOutputs({
bucketName: this.bucketName,
websiteUrl: this.websiteUrl,
});
}
}
좋은 이유: 자식 리소스는 { parent: this }를 사용하고, 이름은 부모에서 접두사로 지정되며, registerOutputs가 호출되고, 타입 토큰은 pkg:module:Type 형식을 따릅니다.
공급자 상속 및 다중 리소스 컴포넌트가 포함된 완전한 컴포넌트 패턴은 examples/core.md를 참조하세요.
패턴 3: Outputs 작업
Outputs는 배포 후 해결되는 값을 나타냅니다. 문자열 연결은 절대 사용하지 말고 interpolate, apply 또는 all을 사용하세요.
// interpolate -- 문자열 빌드를 위한 태그된 템플릿 (선호)
const url = pulumi.interpolate`https://${bucket.bucketRegionalDomainName}/index.html`;
// apply -- 단일 출력 변환
const upper = bucket.id.apply((id) => id.toUpperCase());
// all -- 여러 출력 결합
const endpoint = pulumi
.all([lb.dnsName, listener.port])
.apply(([dns, port]) => `http://${dns}:${port}`);
// 리프팅 -- 리소스 출력의 속성에 직접 접근
const subnetId = vpc.subnets[0].id; // 알려진 속성에 대해 apply 불필요
좋은 이유: interpolate는 Output 값을 투명하게 처리하고, all은 여러 값을 기다리며, 리프팅은 불필요한 apply 호출을 방지합니다.
주의: .apply() 내부에서 생성된 리소스는 pulumi preview에 나타나지 않으며 순서 문제를 일으킬 수 있습니다. 항상 Outputs를 다른 리소스에 대한 입력으로 직접 전달하세요.
Output 패턴, pulumi.output() 래핑 및 apply 안티 패턴에 대한 자세한 내용은 examples/core.md를 참조하세요.
패턴 4: 구성 및 비밀
스택별 값에는 pulumi.Config를 사용하세요. 민감한 데이터에는 requireSecret을 사용하세요. 상태에서 값을 암호화합니다.
const config = new pulumi.Config();
// 일반 구성 값
const region = config.require("region"); // 누락 시 실패
const nodeCount = config.getNumber("nodeCount"); // 누락 시 undefined 반환
// 비밀 값 -- 상태에서 암호화됨
const dbPassword = config.requireSecret("dbPassword");
const apiKey = config.getSecret("apiKey");
// 프로그래밍 방식 값을 비밀로 표시
const connectionString = pulumi.interpolate`postgres://admin:${dbPassword}@${db.endpoint}/mydb`;
// dbPassword가 비밀이므로 connectionString은 자동으로 비밀이 됩니다.
// 값을 명시적으로 비밀로 표시
const token = pulumi.secret(generateToken());
핵심 사항: 비밀에서 파생된 모든 출력은 자동으로 비밀로 표시됩니다. 파생된 값을 다시 표시할 필요가 없습니다.
네임스페이스 구성, 비밀 출력 및 config set CLI 명령어에 대한 자세한 내용은 examples/core.md를 참조하세요.
패턴 5: 리소스 옵션
리소스 옵션은 수명 주기 동작을 제어합니다. 가장 중요한 것들은 다음과 같습니다:
const db = new aws.rds.Instance(
"primary-db",
{
/* ... */
},
{
protect: true, // 실수로 인한 삭제 방지
dependsOn: [vpc, securityGroup], // 명시적 순서 지정
ignoreChanges: ["tags"], // 특정 속성의 드리프트 무시
aliases: [{ name: "old-db-name" }], // 재생성 없이 이름 변경
retainOnDelete: true, // pulumi destroy 시 클라우드 리소스 유지
deleteBeforeReplace: true, // 고유해야 하는 리소스의 경우
replaceOnChanges: ["engine"], // 특정 변경 시 강제 교체
provider: usEastProvider, // 명시적 공급자 (리전, 계정)
},
);
주의: 리소스의 논리적 이름이나 부모를 변경하면 Pulumi가 리소스를 삭제하고 다시 생성합니다. 안전하게 이름을 변경하려면 aliases를 사용하세요.
전체 리소스 옵션 테이블은 reference.md를 참조하세요.
패턴 6: 스택 참조
StackReference를 사용하여 스택 간에 출력을 공유하세요.
// 네트워킹 스택에서: 출력 내보내기
export const vpcId = vpc.id;
export const subnetIds = subnets.map((s) => s.id);
// 애플리케이션 스택에서: 출력 사용
const networkStack = new pulumi.StackReference("myorg/networking/prod");
const vpcId = networkStack.getOutput("vpcId");
const subnetIds = networkStack.getOutput("subnetIds");
// requireOutput은 출력이 존재하지 않으면 실패합니다 (getOutput보다 안전)
const vpcIdRequired = networkStack.requireOutput("vpcId");
스택 참조 패턴 및 getOutputDetails에 대한 자세한 내용은 examples/advanced.md를 참조하세요.
패턴 7: 변환
리소스와 그 자식에 변환을 적용하세요. transforms를 사용하세요 (비권장 transformations 대신). 변환은 type, props, opts가 있는 args 객체를 받고, 수정된 결과를 반환하거나 건너뛰려면 undefined를 반환합니다.
// 리소스 수준: 컴포넌트의 모든 태그 가능한 자식에 태그 적용
const vpc = new MyVpcComponent(
"vpc",
{},
{
transforms: [
(args) => {
if (isTaggable(args.type)) {
return {
props: {
...args.props,
tags: { ...args.props["tags"], ManagedBy: "pulumi" },
},
opts: args.opts,
};
}
return undefined;
},
],
},
);
비권장 transformations와의 주요 차이점: transforms는 패키징된 컴포넌트 자식(awsx, eks) 수정을 지원하고, 비동기 콜백을 지원하며, Resource 객체를 전달하지 않습니다.
스택 수준 변환, 옵션 수정 및 transformations에서 마이그레이션에 대한 자세한 내용은 examples/advanced.md를 참조하세요.
패턴 8: Automation API
CLI 없이 프로그래밍 방식으로 Pulumi를 실행하세요. 자체 서비스 플랫폼, 통합 테스트 또는 사용자 정의 배포 도구에 적합합니다.
const stack = await LocalWorkspace.createOrSelectStack({
stackName: "dev",
projectName: "my-platform",
program: async () => {
const bucket = new aws.s3.Bucket("auto-bucket");
return { bucketName: bucket.id };
},
});
const upResult = await stack.up({ onOutput: console.log });
핵심 사항: Automation API를 사용하려면 Pulumi CLI가 설치되어 있고 PATH에 있어야 합니다. 내부적으로 CLI의 엔진을 사용합니다.
미리 보기, 삭제, 로컬 프로그램 모드, 구성 설정 및 스택 출력 검색에 대한 자세한 내용은 examples/advanced.md를 참조하세요.
<red_flags>
위험 신호
높은 우선 순위:
.apply()내부에서 리소스 생성 --pulumi preview에 나타나지 않고, 순서 문제를 일으키며, 종속성 그래프를 손상시킵니다. Outputs를 리소스 입력으로 직접 전달하세요.- ComponentResource 자식에서
{ parent: this }누락 -- 리소스가 상태 트리의 루트에 나타나 논리적 그룹화와 컴포넌트 삭제 계단식 전파를 손상시킵니다. - Outputs와 문자열 연결 --
"https://" + bucket.id는[object Object]를 생성합니다. 대신pulumi.interpolate를 사용하세요. - 비밀번호/키에
Config.require()사용 -- 값을 일반 텍스트로 상태에 저장합니다. 암호화하려면Config.requireSecret()을 사용하세요. this.registerOutputs()호출 누락 -- 컴포넌트 출력이 상태에서 제대로 추적되지 않거나 스택 참조를 통해 사용할 수 없습니다.- 별칭 없이 논리적 이름 변경 -- Pulumi가 리소스를 삭제하고 다시 생성합니다. 안전하게 이름을 변경하려면
aliases: [{ name: "old-name" }]를 사용하세요.
중간 우선 순위:
- 다중 리전 설정에서 기본 공급자에 의존 -- 리전별로 명시적 공급자를 사용하세요. 강제 적용하려면 구성에서
pulumi:disable-default-providers를 설정하세요. - ComponentResource 대신 함수로 시작 -- 나중에 마이그레이션하려면 별칭 또는 리소스 재생성이 필요합니다. 처음부터 컴포넌트를 사용하세요.
- 명확하지 않은 종속성에
dependsOn을 사용하지 않음 -- Pulumi는 Input/Output 연결에서 종속성을 추론하지만, 부작용(IAM 전파 지연 등)은 수동dependsOn이 필요할 수 있습니다. pulumi config로 설정할 수 없는 하드코딩 값 -- 환경별 구성에 하드코딩된 문자열 대신new pulumi.Config()를 사용하세요.- 암시적
any로 타입 안전성 무시 -- 리소스 속성에any를 사용하지 마세요. 올바른 타입을 사용하고 필요한 경우as로 타입을 단언하세요. - 초기화되지 않은 출력 사용 --
getOutput은 참조된 스택에 출력이 없으면undefined를 반환할 수 있습니다. 런타임 오류를 방지하려면requireOutput을 사용하거나undefined를 확인하세요.
모범 사례가 아닌 것:
- 구성에
.json파일 사용 -- Pulumi 구성 시스템(pulumi configCLI)을 사용하거나getObject를 사용한 JSON 파싱을 사용하세요. - 클래스 정의 외부에서
new Resource()-- 리소스는 항상 적절한 컴포넌트 컨텍스트 내부에 생성되어야 합니다. 전역 리소스는 추적 및 삭제를 어렵게 만듭니다. - 긴 블로킹 작업에
apply내부에서async사용 --.apply()내부의 async 콜백은 Pulumi가 리소스의 등록을 완료할 때까지 기다리지 못하게 할 수 있습니다. 대신pulumi.all을 사용하세요. - 수동 상태 편집 -- 직접 Pulumi 상태 파일을 편집하지 마세요. 상태를 조작해야 하는 경우
pulumi stateCLI 명령어를 사용하세요.
</red_flags>
<setup_and_reference>
설정
# Pulumi CLI 설치
export PULUMI_HOME="$HOME/.pulumi"
curl -fsSL https://get.pulumi.com | sh
# 프로젝트 초기화 (인터랙티브)
pulumi new aws-typescript
# npm 종속성 설치
npm install @pulumi/pulumi @pulumi/aws
# 구성 설정
pulumi config set aws:region us-west-2
pulumi config set dbPassword --secret # 비밀 값
pulumi config set dbPassword admin123 # (다른 명령에서 비밀 값 설정)
# 배포
pulumi up
# 미리 보기 (변경 사항만 표시)
pulumi preview
# 출력 보기
pulumi stack output
pulumi stack output bucketName --json
# 스택 전환 및 제거
pulumi stack select staging
pulumi destroy
pulumi stack rm staging
디렉토리 구조 (권장):
infra/
├── Pulumi.yaml # 프로젝트 메타데이터
├── Pulumi.dev.yaml # dev 스택 구성
├── Pulumi.prod.yaml # prod 스택 구성
├── index.ts # 진입점 (스택 정의)
├── components/ # 재사용 가능한 ComponentResource
│ ├── staticSite.ts
│ └── database.ts
├── config.ts # 구성 추출 함수
└── utils.ts # 헬퍼 함수
일반적인 CLI 명령어 참조
| 명령어 | 설명 |
|---|---|
pulumi new | 새 프로젝트 생성 |
pulumi up | 인프라 배포 또는 업데이트 |
pulumi preview | 변경 사항 미리 보기 |
pulumi destroy | 모든 리소스 삭제 |
pulumi stack | 스택 관리 (선택, 생성, 제거) |
pulumi config | 구성 값 관리 |
pulumi state | 상태 관리 (고급) |
pulumi import | 기존 리소스 가져오기 |
pulumi renew-leases | 작업 중인 업데이트 임대 갱신 |
pulumi logs | 리소스 로그 보기 |
pulumi console | Pulumi 콘솔에서 스택 열기 |
빠른 참조: 일반적인 실수 및 해결 방법
| 실수 | 증상 | 해결 방법 |
|---|---|---|
.apply() 내부의 리소스 | 리소스가 preview에 나타나지 않음, 순서 문제 | Input/Output 종속성 사용 |
누락된 parent: this | 리소스가 최상위에 나타남, 컴포넌트 삭제가 작동하지 않음 | 모든 자식 생성자에 추가 |
| Outputs와 문자열 연결 | [object Object]가 출력에 표시됨 | pulumi.interpolate 사용 |
Config.require()로 비밀 값 | 상태의 일반 텍스트 값 | Config.requireSecret()으로 전환 |
누락된 registerOutputs() | 컴포넌트 출력을 사용할 수 없음 | 생성자 끝에서 호출 |
이름 변경 시 누락된 aliases | 리소스가 삭제되고 다시 생성됨 | aliases: [{ name: "old-name" }] 추가 |
| 프로젝트에서 리소스 이동/파일 분할 | 리소스가 삭제되고 다시 생성됨 | 부모/이름 변경 시 aliases 사용 |
| 교차 리전 또는 교차 계정에서 기본 공급자 사용 | 잘못된 리전의 리소스 | 명시적 공급자 인스턴스 생성 |
추가 리소스
- Pulumi 문서
- AWS Classic 공급자
- AWS Native 공급자
- GCP 공급자
- Azure Native 공급자
- Kubernetes 공급자
- Automation API
- CrossGuard (정책)
- Pulumi 블로그
스킬 메타데이터
- 스킬 이름: Pulumi 인프라를 코드로
- 설명: TypeScript 네이티브 인프라를 코드로 관리합니다.
- 자동 감지 트리거:
pulumi,@pulumi/,ComponentResource,CustomResource,Output,Config,StackReference,Automation API,CrossGuard,PolicyPack - 관련 스킬:
typescript,aws,gcp,azure,kubernetes - 버전: 1.0.0
이 스킬은 Pulumi의 IaC 패턴에 대한 빠른 참조 가이드이지만 포괄적인 문서는 아닙니다. 심층적인 설명은 Pulumi 문서 및 예제 파일을 참조하세요.