324 lines
7.2 KiB
Markdown
324 lines
7.2 KiB
Markdown
---
|
|
id: devops-pulumi-iac
|
|
title: Pulumi — 코드로 IaC (TS / Python / Go)
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [devops, pulumi, iac, vibe-coding]
|
|
tech_stack: { language: "TS / Python / Go", applicable_to: ["DevOps"] }
|
|
applied_in: []
|
|
aliases: [Pulumi, IaC, CDK, AWS CDK, infrastructure as code, programmatic IaC]
|
|
---
|
|
|
|
# Pulumi
|
|
|
|
> Terraform 의 코드 버전. **TS / Python / Go / .NET 으로 IaC**. AWS CDK 비슷 + multi-cloud. Loop / function / abstraction 자유.
|
|
|
|
## 📖 핵심 개념
|
|
- 코드 = infra 정의.
|
|
- State: Pulumi Cloud / S3 / 자체.
|
|
- Stack: 환경별 (dev / prod).
|
|
- Component: 재사용 unit.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### 시작
|
|
```bash
|
|
brew install pulumi
|
|
pulumi new typescript
|
|
# 또는 aws-typescript / azure-typescript / kubernetes-typescript
|
|
```
|
|
|
|
### TS 예 — S3 + RDS
|
|
```ts
|
|
import * as aws from '@pulumi/aws';
|
|
import * as random from '@pulumi/random';
|
|
|
|
// S3
|
|
const bucket = new aws.s3.BucketV2('my-bucket', {
|
|
bucket: 'my-bucket',
|
|
tags: { Env: 'prod' },
|
|
});
|
|
|
|
new aws.s3.BucketServerSideEncryptionConfigurationV2('encrypt', {
|
|
bucket: bucket.id,
|
|
rules: [{ applyServerSideEncryptionByDefault: { sseAlgorithm: 'AES256' } }],
|
|
});
|
|
|
|
// RDS
|
|
const password = new random.RandomPassword('db-password', { length: 32 });
|
|
|
|
const db = new aws.rds.Instance('app-db', {
|
|
engine: 'postgres',
|
|
engineVersion: '16',
|
|
instanceClass: 'db.t4g.micro',
|
|
allocatedStorage: 20,
|
|
storageEncrypted: true,
|
|
username: 'app',
|
|
password: password.result,
|
|
skipFinalSnapshot: false,
|
|
finalSnapshotIdentifier: 'app-db-final',
|
|
deletionProtection: true,
|
|
});
|
|
|
|
export const dbEndpoint = db.endpoint;
|
|
export const bucketName = bucket.id;
|
|
```
|
|
|
|
### 명령
|
|
```bash
|
|
pulumi up # plan + apply
|
|
pulumi up --yes # 확인 없이
|
|
pulumi preview # plan only
|
|
pulumi destroy
|
|
pulumi stack ls
|
|
pulumi stack output dbEndpoint
|
|
```
|
|
|
|
### Stack (환경별)
|
|
```bash
|
|
pulumi stack init dev
|
|
pulumi stack init staging
|
|
pulumi stack init prod
|
|
|
|
pulumi stack select prod
|
|
pulumi up
|
|
```
|
|
|
|
```yaml
|
|
# Pulumi.prod.yaml
|
|
config:
|
|
aws:region: us-east-1
|
|
myapp:dbInstanceClass: db.r6g.large
|
|
myapp:replicaCount: 5
|
|
```
|
|
|
|
```ts
|
|
// Code
|
|
import * as pulumi from '@pulumi/pulumi';
|
|
const cfg = new pulumi.Config('myapp');
|
|
const instanceClass = cfg.require('dbInstanceClass');
|
|
const replicaCount = cfg.requireNumber('replicaCount');
|
|
```
|
|
|
|
### Component (재사용)
|
|
```ts
|
|
class AppDatabase extends pulumi.ComponentResource {
|
|
public readonly endpoint: pulumi.Output<string>;
|
|
|
|
constructor(name: string, args: AppDatabaseArgs, opts?: pulumi.ComponentResourceOptions) {
|
|
super('myco:db:AppDatabase', name, args, opts);
|
|
|
|
const password = new random.RandomPassword(`${name}-pw`, { length: 32 }, { parent: this });
|
|
|
|
const db = new aws.rds.Instance(`${name}-rds`, {
|
|
engine: 'postgres',
|
|
instanceClass: args.instanceClass,
|
|
// ...
|
|
}, { parent: this });
|
|
|
|
new aws.secretsmanager.Secret(`${name}-secret`, { ... }, { parent: this });
|
|
|
|
this.endpoint = db.endpoint;
|
|
this.registerOutputs({ endpoint: this.endpoint });
|
|
}
|
|
}
|
|
|
|
interface AppDatabaseArgs {
|
|
instanceClass: string;
|
|
}
|
|
|
|
// 사용
|
|
const ordersDb = new AppDatabase('orders', { instanceClass: 'db.r6g.large' });
|
|
const inventoryDb = new AppDatabase('inventory', { instanceClass: 'db.t4g.medium' });
|
|
```
|
|
|
|
→ 재사용 + grouping.
|
|
|
|
### Multi-cloud
|
|
```ts
|
|
import * as aws from '@pulumi/aws';
|
|
import * as gcp from '@pulumi/gcp';
|
|
import * as k8s from '@pulumi/kubernetes';
|
|
|
|
// AWS
|
|
const bucket = new aws.s3.BucketV2('logs');
|
|
|
|
// GCP
|
|
const gcsBucket = new gcp.storage.Bucket('analytics', { location: 'US' });
|
|
|
|
// K8s
|
|
const ns = new k8s.core.v1.Namespace('app', { metadata: { name: 'app' } });
|
|
```
|
|
|
|
→ 한 program 안 multi-cloud.
|
|
|
|
### Output (async value)
|
|
```ts
|
|
const url = pulumi.interpolate`https://${db.endpoint}:5432/${dbName}`;
|
|
|
|
// 또는
|
|
const connection = pulumi.all([db.endpoint, password.result]).apply(([endpoint, pw]) =>
|
|
`postgresql://app:${pw}@${endpoint}:5432/app`
|
|
);
|
|
```
|
|
|
|
→ Pulumi 의 lazy / async value handling.
|
|
|
|
### Secret
|
|
```ts
|
|
const apiKey = cfg.requireSecret('apiKey'); // encrypted in state
|
|
```
|
|
|
|
```bash
|
|
pulumi config set --secret myapp:apiKey sk-abc123
|
|
```
|
|
|
|
### Import (existing)
|
|
```bash
|
|
pulumi import aws:s3/bucket:Bucket existing my-existing-bucket
|
|
```
|
|
|
|
→ 기존 cloud 자원 → Pulumi 안 가져오기.
|
|
|
|
### Drift detection
|
|
```bash
|
|
pulumi refresh # cloud → state sync
|
|
pulumi up # state → cloud sync
|
|
```
|
|
|
|
### CI
|
|
```yaml
|
|
- uses: pulumi/actions@v5
|
|
with:
|
|
command: up
|
|
stack-name: prod
|
|
cloud-url: s3://my-state-bucket
|
|
```
|
|
|
|
### vs Terraform
|
|
```
|
|
Pulumi:
|
|
+ Real programming language (TS / Python)
|
|
+ Loop / function / abstraction
|
|
+ Test (Jest)
|
|
+ Type-safe (TS)
|
|
- Smaller community
|
|
- Newer
|
|
|
|
Terraform:
|
|
+ HCL (declarative, simpler 작은 case)
|
|
+ Largest community
|
|
+ Module marketplace
|
|
- HCL 의 한계 (loop, complex logic)
|
|
```
|
|
|
|
→ 큰 / 복잡 = Pulumi. 단순 / 표준 = Terraform.
|
|
|
|
### Test
|
|
```ts
|
|
import { describe, it } from '@jest/globals';
|
|
import * as pulumi from '@pulumi/pulumi';
|
|
|
|
pulumi.runtime.setMocks({
|
|
newResource: (args) => ({
|
|
id: `${args.name}-id`,
|
|
state: { ...args.inputs, id: `${args.name}-id` },
|
|
}),
|
|
call: () => ({}),
|
|
});
|
|
|
|
describe('infrastructure', () => {
|
|
it('creates encrypted bucket', async () => {
|
|
const infra = await import('./index');
|
|
const bucket = infra.bucket;
|
|
const sseConfig = await new Promise((resolve) => bucket.serverSideEncryptionConfiguration.apply(resolve));
|
|
expect(sseConfig).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
→ 일반 unit test 처럼.
|
|
|
|
### Component packages (sharing)
|
|
```bash
|
|
# Component 를 npm package 로
|
|
yarn publish
|
|
```
|
|
|
|
```ts
|
|
// 다른 곳
|
|
import { AppDatabase } from '@myco/pulumi-components';
|
|
const db = new AppDatabase('orders', {...});
|
|
```
|
|
|
|
### Crossplane vs Pulumi
|
|
```
|
|
Crossplane: K8s 안 cloud manage.
|
|
Pulumi: Code 로 cloud manage.
|
|
|
|
→ K8s native = Crossplane. Code-first = Pulumi.
|
|
```
|
|
|
|
### AWS CDK
|
|
```ts
|
|
// CDK = AWS only Pulumi-like
|
|
import { Stack } from 'aws-cdk-lib';
|
|
import { Bucket } from 'aws-cdk-lib/aws-s3';
|
|
|
|
class MyStack extends Stack {
|
|
constructor(...) {
|
|
super(...);
|
|
new Bucket(this, 'MyBucket');
|
|
}
|
|
}
|
|
```
|
|
|
|
→ AWS only. AWS deeply 통합.
|
|
|
|
### Best practices
|
|
```
|
|
1. State = remote (Pulumi Cloud / S3).
|
|
2. Stack 별 환경 분리.
|
|
3. Component 로 재사용.
|
|
4. Test (mock).
|
|
5. CI 자동.
|
|
6. Secret 명시 (encrypted state).
|
|
7. Import existing 가능.
|
|
8. Drift 정기 detect.
|
|
```
|
|
|
|
## 🤔 의사결정 기준
|
|
| 상황 | 추천 |
|
|
|---|---|
|
|
| Code-first IaC | Pulumi |
|
|
| AWS only | Pulumi 또는 CDK |
|
|
| 일반 / 표준 | Terraform / OpenTofu |
|
|
| K8s 중심 | Crossplane |
|
|
| Multi-cloud | Pulumi |
|
|
| TS team | Pulumi (자연) |
|
|
| Module marketplace | Terraform |
|
|
|
|
## ❌ 안티패턴
|
|
- **State local file**: 잃으면 disaster. Remote.
|
|
- **Stack mix (dev / prod 한 곳)**: 분리.
|
|
- **Secret plain config**: requireSecret.
|
|
- **Drift 무 detect**: 콘솔 변경 → 다음 up 가 덮음.
|
|
- **Component 없이 copy-paste**: 코드 폭발.
|
|
- **Test 없이 prod**: 위험.
|
|
- **Outputs share 안 함**: 다른 stack 가 reference 못 함.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- TS = type-safe IaC.
|
|
- Component 적극.
|
|
- Stack per env.
|
|
- Pulumi Cloud free tier.
|
|
|
|
## 🔗 관련 문서
|
|
- [[DevOps_Terraform_Patterns]]
|
|
- [[DevOps_Crossplane_Tekton]]
|
|
- [[DevOps_ArgoCD_GitOps]]
|