메인 μ½˜ν…μΈ λ‘œ κ±΄λ„ˆλ›°κΈ°
Sonamu ν”„λ‘œμ νŠΈμ—μ„œ μ™ΈλΆ€ μ„œλΉ„μŠ€ (AWS S3, OpenAI λ“±)λ₯Ό μ‚¬μš©ν•  λ•Œ API ν‚€λ₯Ό ν™˜κ²½λ³€μˆ˜λ‘œ κ΄€λ¦¬ν•©λ‹ˆλ‹€. μ½”λ“œμ— ν•˜λ“œμ½”λ”©ν•˜μ§€ μ•Šκ³  μ•ˆμ „ν•˜κ²Œ λ³΄κ΄€ν•˜λŠ” 방법을 μ„€λͺ…ν•©λ‹ˆλ‹€.
API ν‚€λŠ” μ ˆλŒ€ Git에 μ»€λ°‹ν•˜λ©΄ μ•ˆ λ©λ‹ˆλ‹€! 유좜되면 μ¦‰μ‹œ ν‚€λ₯Ό λ¬΄νš¨ν™”ν•˜κ³  μž¬λ°œκΈ‰λ°›μœΌμ„Έμš”.

μ£Όμš” API ν‚€

Sonamuμ—μ„œ 자주 μ‚¬μš©ν•˜λŠ” μ™ΈλΆ€ μ„œλΉ„μŠ€ API ν‚€μž…λ‹ˆλ‹€.

AWS (S3 μŠ€ν† λ¦¬μ§€)

.env
# AWS 자격 증λͺ…
AWS_ACCESS_KEY_ID=your-aws-access-key-id
AWS_SECRET_ACCESS_KEY=your-aws-secret-access-key

# S3 μ„€μ •
S3_REGION=ap-northeast-2
S3_BUCKET=my-project-uploads
sonamu.config.ts:
import { defineConfig } from "sonamu";
import { drivers } from "sonamu/storage";

export default defineConfig({
  server: {
    storage: {
      default: process.env.DRIVE_DISK ?? "fs",
      drivers: {
        s3: drivers.s3({
          credentials: {
            accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
          },
          region: process.env.S3_REGION ?? "ap-northeast-2",
          bucket: process.env.S3_BUCKET ?? "default-bucket",
          visibility: "private",
        }),
      },
    },
  },
});

OpenAI

.env
# OpenAI API ν‚€
OPENAI_API_KEY=your-openai-api-key
OPENAI_ORG_ID=your-openai-org-id  # Organization ID (선택)
μ‚¬μš© μ˜ˆμ‹œ:
import OpenAI from "openai";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  organization: process.env.OPENAI_ORG_ID,
});

const completion = await openai.chat.completions.create({
  model: "gpt-4",
  messages: [{ role: "user", content: "Hello!" }],
});

선택적 μ™ΈλΆ€ μ„œλΉ„μŠ€ 톡합

SonamuλŠ” λ‹€μ–‘ν•œ μ™ΈλΆ€ μ„œλΉ„μŠ€λ₯Ό 톡합할 수 μžˆμŠ΅λ‹ˆλ‹€. μ•„λž˜λŠ” 자주 μ‚¬μš©λ˜λŠ” μ„œλΉ„μŠ€λ“€μ˜ ν™˜κ²½λ³€μˆ˜ μ˜ˆμ‹œμž…λ‹ˆλ‹€.
이 μ„œλΉ„μŠ€λ“€μ€ Sonamu κΈ°λ³Έ ꡬ성이 μ•„λ‹™λ‹ˆλ‹€. ν”„λ‘œμ νŠΈμ— ν•„μš”ν•œ κ²½μš°μ—λ§Œ μΆ”κ°€ν•˜μ„Έμš”.
# Stripe 결제
STRIPE_SECRET_KEY=your-stripe-secret-key
STRIPE_PUBLISHABLE_KEY=your-stripe-publishable-key
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret

ν™˜κ²½λ³„ ν‚€ 관리

μ ˆλŒ€ ν”„λ‘œλ•μ…˜ ν‚€λ₯Ό 개발 ν™˜κ²½μ— μ‚¬μš©ν•˜μ§€ λ§ˆμ„Έμš”! 각 ν™˜κ²½λ§ˆλ‹€ λ³„λ„μ˜ ν‚€λ₯Ό μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.
.env.development
# ν…ŒμŠ€νŠΈμš© ν‚€ λ˜λŠ” μƒŒλ“œλ°•μŠ€ ν‚€
OPENAI_API_KEY=your-test-openai-key

AWS_ACCESS_KEY_ID=your-test-aws-key-id
AWS_SECRET_ACCESS_KEY=your-test-aws-secret-key
S3_BUCKET=myproject-dev

STRIPE_SECRET_KEY=your-test-stripe-key
νŠΉμ§•:
  • ν…ŒμŠ€νŠΈ λͺ¨λ“œ ν‚€ μ‚¬μš©
  • μš”κΈˆ λΆ€κ³Ό μ—†κ±°λ‚˜ μ΅œμ†Œν™”
  • μ œν•œλœ κΆŒν•œ

AWS IAM μ‚¬μš©μž 생성

IAM μ½˜μ†” 접속

AWS IAM Console 접속

μ‚¬μš©μž 생성

  1. Users β†’ Add users 2. μ‚¬μš©μž 이름: sonamu-s3-user 3. Access type: Programmatic access 선택

κΆŒν•œ μ„€μ •

Option 1: κΈ°μ‘΄ μ •μ±… μ—°κ²°
AmazonS3FullAccess  ❌ (λ„ˆλ¬΄ κ΄‘λ²”μœ„)
Option 2: μ»€μŠ€ν…€ μ •μ±… (ꢌμž₯)
sonamu-s3-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::myproject-uploads/*",
        "arn:aws:s3:::myproject-uploads"
      ]
    }
  ]
}
νŠΉμ • 버킷과 μž‘μ—…λ§Œ ν—ˆμš©ν•˜μ—¬ λ³΄μ•ˆμ„ κ°•ν™”ν•˜μ„Έμš”.

μ•‘μ„ΈμŠ€ ν‚€ λ°œκΈ‰

μ‚¬μš©μž 생성 μ™„λ£Œ ν›„:
  • Access key ID: your-aws-access-key-id
  • Secret access key: your-aws-secret-access-key
Secret access keyλŠ” ν•œ 번만 ν‘œμ‹œλ©λ‹ˆλ‹€! μ¦‰μ‹œ μ•ˆμ „ν•œ 곳에 μ €μž₯ν•˜μ„Έμš”.

.env νŒŒμΌμ— μΆ”κ°€

AWS_ACCESS_KEY_ID=your-aws-access-key-id
AWS_SECRET_ACCESS_KEY=your-aws-secret-access-key

OpenAI API ν‚€ λ°œκΈ‰

OpenAI ν”Œλž«νΌ 접속

OpenAI API Platform 둜그인

API ν‚€ 생성

  1. API keys β†’ Create new secret key 2. 이름: sonamu-production 3. κΆŒν•œ: All λ˜λŠ” Restricted (ꢌμž₯)

μ‚¬μš© ν•œλ„ μ„€μ •

Settings β†’ Billing β†’ Usage limits μ„€μ •
Hard limit: $100/month (초과 μ‹œ μžλ™ 차단)
Soft limit: $80/month (μ•Œλ¦Ό)
μ‚¬μš© ν•œλ„λ₯Ό μ„€μ •ν•˜μ§€ μ•ŠμœΌλ©΄ μ˜ˆμƒμΉ˜ λͺ»ν•œ μš”κΈˆμ΄ 청ꡬ될 수 μžˆμŠ΅λ‹ˆλ‹€!

ν‚€ μ €μž₯

OPENAI_API_KEY=your-openai-api-key
λ°œκΈ‰ μ¦‰μ‹œ μ•ˆμ „ν•œ 곳에 μ €μž₯ (λ‹€μ‹œ 확인 λΆˆκ°€)

λ³΄μ•ˆ λͺ¨λ²” 사둀

정기적인 ν‚€ ꡐ체

ꢌμž₯ μ£ΌκΈ°:
  • ν”„λ‘œλ•μ…˜: 3κ°œμ›”λ§ˆλ‹€
  • μŠ€ν…Œμ΄μ§•: 6κ°œμ›”λ§ˆλ‹€
  • 개발: 1λ…„λ§ˆλ‹€ λ˜λŠ” ν•„μš” μ‹œ

λ‘œν…Œμ΄μ…˜ 절차

1

μƒˆ ν‚€ 생성

κΈ°μ‘΄ ν‚€λ₯Ό μœ μ§€ν•œ 채 μƒˆ ν‚€λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
2

μƒˆ ν‚€ 배포

# μƒˆ ν‚€λ₯Ό ν™˜κ²½λ³€μˆ˜μ— μΆ”κ°€
AWS_ACCESS_KEY_ID=your-new-aws-key-id
AWS_SECRET_ACCESS_KEY=your-new-aws-secret-key

# μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μž¬μ‹œμž‘
3

λͺ¨λ‹ˆν„°λ§

24μ‹œκ°„ λ™μ•ˆ 였λ₯˜ 없이 μž‘λ™ν•˜λŠ”μ§€ 확인
4

이전 ν‚€ μ‚­μ œ

λ¬Έμ œμ—†μœΌλ©΄ 이전 ν‚€ λΉ„ν™œμ„±ν™”/μ‚­μ œ
ν‚€ λ‘œν…Œμ΄μ…˜ μ‹œ λ‹€μš΄νƒ€μž„ 없이 μ§„ν–‰ν•˜λ €λ©΄ μƒˆ 킀와 이전 ν‚€λ₯Ό λ™μ‹œμ— μœ μ§€ν•˜λŠ” 블루-κ·Έλ¦° 방식을 μ‚¬μš©ν•˜μ„Έμš”.

IAM μ •μ±… μ΅œμ†Œν™”

// ❌ λ‚˜μœ 예: λͺ¨λ“  κΆŒν•œ
{
  "Effect": "Allow",
  "Action": "s3:*",
  "Resource": "*"
}

// βœ… 쒋은 예: ν•„μš”ν•œ κΆŒν•œλ§Œ
{
  "Effect": "Allow",
  "Action": [
    "s3:PutObject",
    "s3:GetObject"
  ],
  "Resource": "arn:aws:s3:::my-bucket/uploads/*"
}

읽기 μ „μš© ν‚€ 뢄리

# μ“°κΈ° κΆŒν•œ (μ• ν”Œλ¦¬μΌ€μ΄μ…˜)
AWS_WRITE_ACCESS_KEY_ID=your-write-key-id
AWS_WRITE_SECRET_ACCESS_KEY=your-write-secret-key

# 읽기 κΆŒν•œ (뢄석/λ°±μ—…)
AWS_READ_ACCESS_KEY_ID=your-read-key-id
AWS_READ_SECRET_ACCESS_KEY=your-read-secret-key

μ¦‰μ‹œ 쑰치 사항

ν‚€ μ¦‰μ‹œ λ¬΄νš¨ν™”

# AWS CLI
aws iam delete-access-key \
  --access-key-id your-leaked-access-key-id \
  --user-name sonamu-user

# OpenAI Platform
# API keys β†’ Revoke 클릭

μƒˆ ν‚€ λ°œκΈ‰ 및 배포

μƒˆ ν‚€λ₯Ό μ¦‰μ‹œ λ°œκΈ‰ν•˜κ³  ν”„λ‘œλ•μ…˜μ— 배포

μ‚¬μš© λ‚΄μ—­ 확인

# AWS CloudTrailμ—μ„œ μ˜μ‹¬μŠ€λŸ¬μš΄ ν™œλ™ 확인
# OpenAI Usage νŽ˜μ΄μ§€μ—μ„œ 비정상적인 호좜 확인

Git 이λ ₯ 정리

# 컀밋 이λ ₯μ—μ„œ ν‚€ 제거 (μ‹ μ€‘ν•˜κ²Œ!)
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch .env" \
  --prune-empty --tag-name-filter cat -- --all

# κ°•μ œ ν‘Έμ‹œ
git push origin --force --all
Git 이λ ₯ μ •λ¦¬λŠ” ν˜‘μ—… 쀑인 νŒ€μ— 영ν–₯을 μ€λ‹ˆλ‹€. νŒ€μ›λ“€κ³Ό 쑰율 ν›„ μ§„ν–‰ν•˜μ„Έμš”.

유좜 감지 도ꡬ

# git-secrets μ„€μΉ˜
brew install git-secrets

# ν”„λ‘œμ νŠΈμ— 적용
git secrets --install
git secrets --register-aws

# μŠ€μΊ”
git secrets --scan

개발 ν™˜κ²½

# dotenv-vault μ‚¬μš©
npm install -g dotenv-vault

# μ•”ν˜Έν™”
npx dotenv-vault encrypt

# λ³΅ν˜Έν™” (νŒ€μ›)
DOTENV_KEY=your-dotenv-vault-key npx dotenv-vault decrypt

ν”„λ‘œλ•μ…˜ ν™˜κ²½

AWS Systems Manager Parameter Store:
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";

const client = new SSMClient({ region: "ap-northeast-2" });

async function getSecret(name: string) {
  const command = new GetParameterCommand({
    Name: name,
    WithDecryption: true,
  });
  const response = await client.send(command);
  return response.Parameter?.Value;
}

// μ‚¬μš©
const apiKey = await getSecret("/myproject/prod/openai-api-key");
HashiCorp Vault:
import vault from "node-vault";

const client = vault({
  endpoint: "https://vault.example.com:8200",
  token: process.env.VAULT_TOKEN,
});

const { data } = await client.read("secret/data/myproject/prod");
const apiKey = data.data.OPENAI_API_KEY;

ν‚€ μ‚¬μš© λͺ¨λ‹ˆν„°λ§

AWS CloudWatch

// AWS SDK둜 μ‚¬μš©λŸ‰ λͺ¨λ‹ˆν„°λ§
import { CloudWatchClient, GetMetricStatisticsCommand } from "@aws-sdk/client-cloudwatch";

const client = new CloudWatchClient({ region: "ap-northeast-2" });

const stats = await client.send(
  new GetMetricStatisticsCommand({
    Namespace: "AWS/S3",
    MetricName: "NumberOfObjects",
    Dimensions: [
      {
        Name: "BucketName",
        Value: "my-project-uploads",
      },
    ],
    StartTime: new Date(Date.now() - 86400000), // 24μ‹œκ°„ μ „
    EndTime: new Date(),
    Period: 3600, // 1μ‹œκ°„
    Statistics: ["Average"],
  }),
);

OpenAI μ‚¬μš©λŸ‰ μ•Œλ¦Ό

// μ‚¬μš©λŸ‰ 확인 및 μ•Œλ¦Ό
import OpenAI from "openai";

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// 주기적으둜 μ‹€ν–‰ (cron job)
async function checkUsage() {
  const usage = await fetch("https://api.openai.com/v1/usage", {
    headers: {
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    },
  }).then((r) => r.json());

  const totalCost = usage.total_usage * 0.0001; // μ˜ˆμ‹œ μš”κΈˆ

  if (totalCost > 80) {
    // κ²½κ³  μ•Œλ¦Ό 전솑 (Slack, Email λ“±)
    console.warn(`⚠️ OpenAI μ‚¬μš©λŸ‰ κ²½κ³ : $${totalCost}`);
  }
}

ν…ŒμŠ€νŠΈ ν™˜κ²½ ν‚€ 관리

# .env.test
OPENAI_API_KEY=test-mock-key
AWS_ACCESS_KEY_ID=test-key-id
AWS_SECRET_ACCESS_KEY=test-secret-key
ν…ŒμŠ€νŠΈμ—μ„œλŠ” μ‹€μ œ APIλ₯Ό ν˜ΈμΆœν•˜μ§€ μ•Šλ„λ‘ Mock을 μ‚¬μš©ν•˜μ„Έμš”. ν…ŒμŠ€νŠΈ λΉ„μš©μ„ μ ˆκ°ν•˜κ³  속도λ₯Ό 높일 수 μžˆμŠ΅λ‹ˆλ‹€.

문제 ν•΄κ²°

증상:
Error: The AWS Access Key Id you provided does not exist in our records
원인:
  1. 잘λͺ»λœ Access Key ID
  2. ν‚€κ°€ μ‚­μ œλ˜μ—ˆκ±°λ‚˜ λΉ„ν™œμ„±ν™”λ¨
  3. 타이핑 였λ₯˜ (곡백, μ€„λ°”κΏˆ)
ν•΄κ²°:
# ν‚€ 확인
echo $AWS_ACCESS_KEY_ID

# .env 파일 확인
cat .env | grep AWS

# IAM μ½˜μ†”μ—μ„œ ν‚€ μƒνƒœ 확인
# Active μƒνƒœμΈμ§€ 확인
증상:
RateLimitError: Rate limit reached for gpt-4
원인:
  • API μš”μ²­ ν•œλ„ 초과 (RPM, TPM)
ν•΄κ²°:
// μž¬μ‹œλ„ 둜직 κ΅¬ν˜„
import { RateLimiter } from "limiter";

const limiter = new RateLimiter({
  tokensPerInterval: 10,
  interval: "minute",
});

async function callOpenAI(prompt: string) {
  await limiter.removeTokens(1);

  try {
    return await openai.chat.completions.create({
      model: "gpt-4",
      messages: [{ role: "user", content: prompt }],
    });
  } catch (error) {
    if (error.status === 429) {
      // 1λΆ„ ν›„ μž¬μ‹œλ„
      await new Promise((resolve) => setTimeout(resolve, 60000));
      return callOpenAI(prompt);
    }
    throw error;
  }
}
증상:
Access Denied: You do not have permission to perform this action
원인:
  • IAM 정책에 ν•„μš”ν•œ κΆŒν•œ μ—†μŒ
  • 버킷 μ •μ±…μœΌλ‘œ 차단됨
ν•΄κ²°:
// IAM 정책에 κΆŒν•œ μΆ”κ°€
{
  "Effect": "Allow",
  "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
  "Resource": "arn:aws:s3:::my-bucket/*"
}
# κΆŒν•œ ν…ŒμŠ€νŠΈ
aws s3 ls s3://my-bucket --profile myproject

λ‹€μŒ 단계

.env μ„€μ •

ν™˜κ²½λ³€μˆ˜ κΈ°λ³Έ 섀정을 ν•™μŠ΅ν•˜μ„Έμš”

λ°μ΄ν„°λ² μ΄μŠ€ 인증

DB μ—°κ²° 정보λ₯Ό μ•ˆμ „ν•˜κ²Œ κ΄€λ¦¬ν•˜μ„Έμš”

μŠ€ν† λ¦¬μ§€ μ„€μ •

S3 μŠ€ν† λ¦¬μ§€λ₯Ό μ„€μ •ν•˜μ„Έμš”

μ„Έμ…˜ μ„€μ •

μ„Έμ…˜ 및 λ³΄μ•ˆ 섀정을 ν™•μΈν•˜μ„Έμš”