[인턴] Config와 Pulumi import

Pulumi 실전 가이드: Config 기반 조건 분기와 pulumi import

Pulumi에서 하나의 코드베이스로 여러 환경을 관리하는 방법과, 기존 AWS 리소스를 Pulumi로 편입시키는 import 기능을 실전 예시와 함께 정리합니다.


Part 1. Config 기반 조건 분기

왜 필요한가

Pulumi 프로젝트에서 TypeScript 코드는 단 하나입니다. 그런데 현실에서는 dev 환경에는 RDS가 없고, staging에는 있고, prod에서는 Multi-AZ RDS가 필요한 것처럼 환경마다 인프라 구성이 달라야 합니다.

이 문제를 해결하는 방법이 Config 기반 조건 분기입니다. 각 Stack의 yaml 파일에 설정값을 넣어두고, 코드에서 그 값을 읽어 리소스를 생성할지 말지, 어떤 스펙으로 생성할지를 결정하는 패턴입니다.


Config 시스템의 구조

Pulumi의 Config는 프로젝트 루트에 Pulumi.<stack-name>.yaml 파일로 관리됩니다.

my-infra/
├── index.ts
├── Pulumi.yaml              ← 프로젝트 메타 정보
├── Pulumi.dev-01.yaml       ← dev-01 stack 전용 설정
├── Pulumi.dev-02.yaml       ← dev-02 stack 전용 설정
└── Pulumi.prod.yaml         ← prod stack 전용 설정

config 값을 설정하는 방법은 두 가지입니다.

CLI로 설정하는 방법:

pulumi stack select dev-01
pulumi config set instanceType t3.medium
pulumi config set enableRds false
pulumi config set --secret dbPassword "P@ssw0rd"   # 민감 정보는 --secret

yaml 파일을 직접 편집하는 방법:

# Pulumi.dev-01.yaml
config:
  my-infra:instanceType: t3.medium
  my-infra:enableRds: "false"
  aws:region: ap-northeast-2

여기서 my-infra: 접두사는 프로젝트 이름(Pulumi.yaml의 name)이며, aws:는 aws provider의 네임스페이스입니다.


Config 값을 코드에서 읽는 방법

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

const config = new pulumi.Config();           // 프로젝트 네임스페이스의 config
const awsConfig = new pulumi.Config("aws");   // aws 네임스페이스의 config

// 기본 읽기
const instanceType = config.require("instanceType");       // 없으면 에러
const enableRds = config.requireBoolean("enableRds");      // boolean으로 파싱
const maxCount = config.getNumber("maxCount") ?? 3;        // 없으면 기본값 3

// Secret 읽기
const dbPassword = config.requireSecret("dbPassword");     // Output<string> 반환

// Stack 이름 자체도 활용 가능
const stack = pulumi.getStack();  // "dev-01", "dev-02", "prod" 등

require는 값이 없으면 즉시 에러를 발생시키고, getundefined를 반환합니다. 필수 설정에는 require, 선택적 설정에는 get을 사용하는 것이 관례입니다.


조건 분기 패턴 4가지

패턴 1: 리소스 생성 여부를 on/off 하기

가장 기본적인 패턴입니다. 특정 리소스를 이 stack에서 만들 것인지 말 것인지를 boolean으로 제어합니다.

# Pulumi.dev-01.yaml
config:
  my-infra:enableRds: "false"
  my-infra:enableRedis: "false"

# Pulumi.dev-02.yaml
config:
  my-infra:enableRds: "true"
  my-infra:enableRedis: "true"
const enableRds = config.requireBoolean("enableRds");
const enableRedis = config.requireBoolean("enableRedis");

let rdsInstance: aws.rds.Instance | undefined;
if (enableRds) {
    rdsInstance = new aws.rds.Instance(`db-${stack}`, {
        engine: "mysql",
        instanceClass: config.require("rdsInstanceClass"),
        allocatedStorage: 20,
        skipFinalSnapshot: stack !== "prod",  // prod만 final snapshot
        tags: { Environment: stack },
    });
}

let redisCluster: aws.elasticache.Cluster | undefined;
if (enableRedis) {
    redisCluster = new aws.elasticache.Cluster(`cache-${stack}`, {
        engine: "redis",
        nodeType: "cache.t3.micro",
        numCacheNodes: 1,
    });
}

이 패턴의 핵심은 리소스를 담는 변수를 let으로 선언하고 조건부로 할당하는 것입니다. 이후에 다른 리소스가 이 값을 참조할 때는 undefined 체크를 해야 합니다.


패턴 2: 동일 리소스의 스펙만 변경하기

모든 환경에 EC2가 존재하지만 인스턴스 타입, 디스크 크기 등이 다른 경우입니다.

# Pulumi.dev-01.yaml
config:
  my-infra:instanceType: t3.medium
  my-infra:volumeSize: "20"
  my-infra:instanceCount: "1"

# Pulumi.prod.yaml
config:
  my-infra:instanceType: c5.2xlarge
  my-infra:volumeSize: "100"
  my-infra:instanceCount: "3"
const instanceType = config.require("instanceType");
const volumeSize = config.requireNumber("volumeSize");
const instanceCount = config.requireNumber("instanceCount");

for (let i = 0; i < instanceCount; i++) {
    new aws.ec2.Instance(`web-${stack}-${i}`, {
        ami: "ami-0abcdef1234567890",
        instanceType: instanceType,
        rootBlockDevice: {
            volumeSize: volumeSize,
            volumeType: "gp3",
        },
        tags: {
            Name: `web-${stack}-${i}`,
            Environment: stack,
        },
    });
}

패턴 3: 구조화된 Config 객체 사용하기

설정 항목이 많아지면 개별 config key가 관리하기 어려워집니다. 이때 JSON 객체를 config에 넣을 수 있습니다.

pulumi config set --path 'ec2.instanceType' t3.xlarge
pulumi config set --path 'ec2.volumeSize' 50
pulumi config set --path 'rds.enabled' true
pulumi config set --path 'rds.instanceClass' db.t3.large
pulumi config set --path 'rds.multiAz' false

결과 yaml:

config:
  my-infra:ec2:
    instanceType: t3.xlarge
    volumeSize: 50
  my-infra:rds:
    enabled: true
    instanceClass: db.t3.large
    multiAz: false

코드에서 읽을 때:

// 인터페이스 정의
interface Ec2Config {
    instanceType: string;
    volumeSize: number;
}

interface RdsConfig {
    enabled: boolean;
    instanceClass: string;
    multiAz: boolean;
}

const ec2Conf = config.requireObject<Ec2Config>("ec2");
const rdsConf = config.requireObject<RdsConfig>("rds");

const server = new aws.ec2.Instance(`web-${stack}`, {
    instanceType: ec2Conf.instanceType,
    rootBlockDevice: { volumeSize: ec2Conf.volumeSize },
    // ...
});

if (rdsConf.enabled) {
    new aws.rds.Instance(`db-${stack}`, {
        instanceClass: rdsConf.instanceClass,
        multiAz: rdsConf.multiAz,
        // ...
    });
}

Config 항목이 5개를 넘어가기 시작하면 이 패턴으로 전환하는 것이 좋습니다. 타입 안전성도 확보되고 관련 설정끼리 묶이므로 가독성이 올라갑니다.


패턴 4: Stack 이름 자체로 분기하기

간단한 경우에는 config 없이 stack 이름만으로 분기할 수도 있습니다. 다만 이 방식은 stack 이름에 로직이 결합되므로 유연성이 떨어집니다. 환경 수가 적고 구분이 명확할 때만 사용하세요.

const stack = pulumi.getStack();

const isProd = stack === "prod";
const isStaging = stack.startsWith("staging");

const instance = new aws.ec2.Instance(`web-${stack}`, {
    instanceType: isProd ? "c5.2xlarge" : "t3.medium",
    monitoring: isProd,   // prod만 상세 모니터링
    // ...
});

// prod에서만 Multi-AZ RDS
if (isProd) {
    new aws.rds.Instance(`db-${stack}`, {
        multiAz: true,
        backupRetentionPeriod: 7,
        // ...
    });
}

Config 분기 시 주의사항

1) 코드 변경 후 반드시 모든 활성 stack에서 preview를 실행하세요.

for stack in dev-01 dev-02 staging prod; do
    echo "=== Checking $stack ==="
    pulumi stack select $stack
    pulumi preview 2>&1 | tail -5
    echo ""
done

dev-02를 위해 추가한 코드가 dev-01에서 예상치 못한 변경을 일으킬 수 있습니다.

2) Config 기본값을 코드에 하드코딩하지 마세요.

// 나쁜 예: 코드에서 기본값을 숨김
const instanceType = config.get("instanceType") || "t3.medium";

// 좋은 예: 명시적으로 필수 값으로 관리
const instanceType = config.require("instanceType");

기본값이 코드에 묻히면 나중에 “이 stack에 왜 t3.medium이 떠있지?”라는 상황이 발생합니다. config에 명시적으로 넣어두는 것이 추적에 유리합니다.

3) 조건부 리소스의 export 처리:

// 나쁜 예: rds가 undefined이면 런타임 에러
export const rdsEndpoint = rdsInstance.endpoint;

// 좋은 예: 조건부 export
export const rdsEndpoint = rdsInstance?.endpoint ?? "N/A";


Part 2. pulumi import — 기존 AWS 리소스 가져오기

왜 필요한가

현실에서는 인프라가 항상 IaC로 시작하지 않습니다. AWS 콘솔에서 수동으로 만든 EC2, 운영팀이 CLI로 띄운 RDS, 이전 팀이 CloudFormation으로 만들었지만 이제 Pulumi로 전환하고 싶은 리소스 등 이미 존재하는 리소스를 Pulumi의 state로 편입시켜야 할 때가 있습니다.

pulumi import는 이 작업을 수행합니다. 기존 AWS 리소스를 삭제하거나 재생성하지 않고, Pulumi의 state 파일에 “이 리소스는 내가 관리한다”라고 등록하는 것입니다.


import의 작동 원리

┌─────────────────────┐
│  AWS에 실제 존재하는  │
│  리소스 (EC2 등)     │
└─────────┬───────────┘
          │  pulumi import
          ▼
┌─────────────────────┐
│  Pulumi State 파일   │  ← 리소스 정보가 기록됨
│  (stack 상태 저장소)   │
└─────────┬───────────┘
          │  코드 작성 필요
          ▼
┌─────────────────────┐
│  index.ts 코드       │  ← import 후 직접 작성하거나
│                     │     자동 생성된 코드를 추가
└─────────────────────┘

중요한 점은 import가 두 가지를 동시에 하지 않는다는 것입니다. import는 state에 리소스를 등록하고, 대응하는 코드를 생성해주지만 코드를 프로젝트에 자동으로 삽입하지는 않습니다. 생성된 코드를 개발자가 직접 프로젝트에 넣어야 합니다.


방법 1: CLI를 통한 개별 리소스 import

가장 기본적인 방법입니다. 리소스 하나씩 import합니다.

기본 문법

pulumi import <pulumi-resource-type> <logical-name> <cloud-resource-id>

각 인자의 의미는 다음과 같습니다.

  • pulumi-resource-type: Pulumi에서의 리소스 타입 (예: aws:ec2/instance:Instance)
  • logical-name: Pulumi 코드에서 사용할 리소스 이름 (개발자가 자유롭게 지정)
  • cloud-resource-id: AWS에서의 실제 리소스 ID

실전 예시: EC2 인스턴스 import

먼저 import할 리소스의 ID를 확인합니다.

# AWS CLI로 기존 인스턴스 ID 확인
aws ec2 describe-instances \
  --filters "Name=tag:Name,Values=my-web-server" \
  --query "Reservations[].Instances[].InstanceId" \
  --output text

# 결과: i-0abc123def456789

import를 실행합니다.

pulumi import aws:ec2/instance:Instance my-web-server i-0abc123def456789

실행하면 Pulumi가 AWS API를 호출해서 해당 인스턴스의 모든 속성을 조회한 후, 다음 두 가지를 수행합니다.

첫째, state에 리소스를 등록합니다. 이후 pulumi stack export로 확인하면 해당 리소스가 state에 포함되어 있습니다.

둘째, 대응하는 TypeScript 코드를 터미널에 출력합니다.

Importing (dev-02)

     Type                      Name            Status
 +   pulumi:pulumi:Stack       my-infra-dev-02 created
 =   aws:ec2/instance:Instance my-web-server   imported (1s)

Resources:
    = 1 imported
    1 unchanged

아래와 같은 코드가 자동 생성되어 출력됩니다:

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

const myWebServer = new aws.ec2.Instance("my-web-server", {
    ami: "ami-0abcdef1234567890",
    instanceType: "t3.medium",
    subnetId: "subnet-0123456789abcdef0",
    vpcSecurityGroupIds: ["sg-0123456789abcdef0"],
    keyName: "my-keypair",
    rootBlockDevice: {
        volumeSize: 20,
        volumeType: "gp3",
        encrypted: true,
    },
    tags: {
        Name: "my-web-server",
    },
});

이 출력된 코드를 index.ts에 복사해 넣어야 합니다. 그래야 다음 pulumi up 때 state와 코드가 일치해서 “no changes”가 나옵니다.

다른 리소스 타입 import 예시

# RDS 인스턴스
pulumi import aws:rds/instance:Instance my-database my-db-instance-id

# S3 버킷
pulumi import aws:s3/bucket:Bucket my-bucket my-bucket-name

# Security Group
pulumi import aws:ec2/securityGroup:SecurityGroup my-sg sg-0123456789abcdef0

# VPC
pulumi import aws:ec2/vpc:Vpc my-vpc vpc-0123456789abcdef0

# SNS Topic
pulumi import aws:sns/topic:Topic my-alerts arn:aws:sns:ap-northeast-2:123456789012:my-alerts

# CloudWatch Alarm
pulumi import aws:cloudwatch/metricAlarm:MetricAlarm my-alarm my-cpu-alarm

리소스 타입별로 ID 형식이 다릅니다. EC2는 i-xxxxx, SNS는 ARN, RDS는 인스턴스 식별자 등입니다. 각 리소스의 ID 형식은 Pulumi 공식 문서의 해당 리소스 페이지 하단 Import 섹션에서 확인할 수 있습니다.


방법 2: 코드에서 import 옵션 사용하기

CLI 대신 코드에 import 옵션을 명시하고 pulumi up을 실행하는 방법도 있습니다.

const myServer = new aws.ec2.Instance("my-web-server", {
    ami: "ami-0abcdef1234567890",
    instanceType: "t3.medium",
    subnetId: "subnet-0123456789abcdef0",
    vpcSecurityGroupIds: ["sg-0123456789abcdef0"],
    tags: {
        Name: "my-web-server",
    },
}, {
    import: "i-0abc123def456789",   // ← AWS 리소스 ID
});
pulumi up
# 출력: = aws:ec2/instance:Instance my-web-server imported

import가 완료된 후에는 import 옵션을 코드에서 제거합니다. 남겨두면 다음 pulumi up 때 에러가 발생합니다(이미 state에 존재하므로).

// import 완료 후 정리
const myServer = new aws.ec2.Instance("my-web-server", {
    ami: "ami-0abcdef1234567890",
    instanceType: "t3.medium",
    subnetId: "subnet-0123456789abcdef0",
    vpcSecurityGroupIds: ["sg-0123456789abcdef0"],
    tags: {
        Name: "my-web-server",
    },
    // import 라인 삭제
});

이 방법은 코드를 개발자가 미리 직접 작성해야 하므로 리소스 속성을 정확히 알고 있을 때 적합합니다. AWS 콘솔에서 속성을 하나하나 확인해야 하는 번거로움이 있지만 코드와 state가 동시에 정리되는 장점이 있습니다.


방법 3: Bulk Import (JSON 파일 기반 대량 import)

리소스가 많을 때는 JSON 파일에 목록을 작성하고 한 번에 import할 수 있습니다.

// resources.json
{
    "resources": [
        {
            "type": "aws:ec2/vpc:Vpc",
            "name": "main-vpc",
            "id": "vpc-0123456789abcdef0"
        },
        {
            "type": "aws:ec2/subnet:Subnet",
            "name": "public-subnet-a",
            "id": "subnet-0aaa111bbb222ccc3"
        },
        {
            "type": "aws:ec2/subnet:Subnet",
            "name": "public-subnet-b",
            "id": "subnet-0ddd444eee555fff6"
        },
        {
            "type": "aws:ec2/securityGroup:SecurityGroup",
            "name": "web-sg",
            "id": "sg-0123456789abcdef0"
        },
        {
            "type": "aws:ec2/instance:Instance",
            "name": "web-server",
            "id": "i-0abc123def456789"
        },
        {
            "type": "aws:rds/instance:Instance",
            "name": "main-db",
            "id": "my-database-instance"
        }
    ]
}
pulumi import -f resources.json

모든 리소스에 대한 코드가 한 번에 생성됩니다. 이 방법은 기존 인프라를 Pulumi로 전환하는 마이그레이션 프로젝트에서 특히 유용합니다.


import 후 필수 작업: 코드-State 동기화

import 직후 가장 중요한 작업은 코드와 state를 일치시키는 것입니다.

# import 직후 확인
pulumi preview

이 preview에서 나올 수 있는 결과는 세 가지입니다.

“no changes” — 이상적인 상태입니다. 코드가 실제 리소스와 완벽히 일치합니다.

“update”가 표시됨 — 코드의 속성값이 실제 리소스와 다릅니다. 이 경우 두 가지 선택지가 있습니다.

~ aws:ec2/instance:Instance my-web-server update
    ~ instanceType: "t3.medium" => "t3.large"
  • 선택지 A: 코드를 실제 리소스에 맞추기 — 기존 상태를 유지하고 싶다면 코드에서 t3.mediumt3.large로 수정합니다.
  • 선택지 B: 리소스를 코드에 맞추기 — 스펙 변경이 목적이었다면 그대로 pulumi up을 실행합니다.

“create” + “delete”가 표시됨 — 코드의 리소스 이름이 state와 매칭되지 않습니다. import 시 지정한 logical name과 코드의 리소스 이름이 동일한지 확인하세요.


실전 시나리오: dev-01의 SNS를 dev-02로 가져오기

dev-01에서 Pulumi로 배포한 SNS Topic을 dev-02 stack에서도 동일하게 사용하고 싶은 경우입니다.

방법 A: import로 기존 리소스를 dev-02에 편입

# dev-01에서 SNS ARN 확인
pulumi stack select dev-01
pulumi stack output snsTopicArn
# 출력: arn:aws:sns:ap-northeast-2:123456789012:alerts-dev-01

# dev-02에서 해당 리소스를 import
pulumi stack select dev-02
pulumi import aws:sns/topic:Topic alerts-dev-02 \
  arn:aws:sns:ap-northeast-2:123456789012:alerts-dev-01

단, 이 방법은 하나의 리소스를 두 stack이 관리하는 상황을 만들므로 위험합니다. dev-01에서 pulumi destroy를 하면 dev-02가 참조하는 SNS가 삭제됩니다.

방법 B: Stack Reference (권장)

리소스를 공유할 때는 import보다 Stack Reference가 안전합니다.

// dev-02의 코드
const dev01Ref = new pulumi.StackReference("my-org/my-infra/dev-01");
const sharedSnsArn = dev01Ref.getOutput("snsTopicArn");

// dev-01의 SNS를 alarm action으로 사용
const alarm = new aws.cloudwatch.MetricAlarm(`cpu-alarm-${stack}`, {
    // ...
    alarmActions: [sharedSnsArn],
});

import 주의사항 정리

1) import는 state만 변경하고, 실제 AWS 리소스를 수정하지 않습니다. 안전한 작업이지만, 이후 pulumi up에서 코드와 실제 리소스가 다르면 변경이 발생할 수 있습니다.

2) import 후 반드시 pulumi preview로 drift를 확인하세요. “no changes”가 나올 때까지 코드를 조정하는 것이 안전합니다.

3) protect 옵션을 적극 활용하세요. import한 리소스가 실수로 삭제되는 것을 방지합니다.

const importedDb = new aws.rds.Instance("legacy-db", {
    // ... 속성들
}, {
    protect: true,  // pulumi destroy 시에도 이 리소스는 삭제하지 않음
});

4) retainOnDelete 옵션도 고려하세요. Pulumi state에서 제거하되 실제 AWS 리소스는 남기고 싶을 때 사용합니다.

const importedDb = new aws.rds.Instance("legacy-db", {
    // ... 속성들
}, {
    retainOnDelete: true,  // state에서만 제거, AWS 리소스는 유지
});

5) 리소스 간 의존성 순서를 지켜서 import하세요. VPC → Subnet → Security Group → EC2 순서로 import해야 참조 관계가 올바르게 설정됩니다.


한눈에 보는 비교 요약

항목 Config 기반 조건 분기 pulumi import
목적 하나의 코드로 여러 환경을 관리 기존 AWS 리소스를 Pulumi로 편입
사용 시점 새 Stack 생성 시 환경별 차이 반영 수동 생성된 리소스를 IaC로 전환
변경 대상 Config yaml + 코드 내 분기 로직 Pulumi state + 대응 코드 추가
위험도 낮음 (preview로 사전 확인 가능) 중간 (코드-state 불일치 주의)
함께 쓰는 경우 import 후 config 분기로 관리하면 가장 이상적  

이 두 가지 기능을 조합하면 “기존 AWS 리소스를 import로 가져온 뒤, config 분기로 stack별 차이를 관리”하는 완전한 IaC 전환이 가능합니다.

Categories:

Updated:

Leave a comment