๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
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

๋‹ค์Œ ๋‹จ๊ณ„