Sonamu ํ๋ก์ ํธ์์ ์ธ๋ถ ์๋น์ค (AWS S3, OpenAI ๋ฑ)๋ฅผ ์ฌ์ฉํ ๋ API ํค๋ฅผ ํ๊ฒฝ๋ณ์๋ก ๊ด๋ฆฌํฉ๋๋ค. ์ฝ๋์ ํ๋์ฝ๋ฉํ์ง ์๊ณ ์์ ํ๊ฒ ๋ณด๊ดํ๋ ๋ฐฉ๋ฒ์ ์ค๋ช
ํฉ๋๋ค.
API ํค๋ ์ ๋ Git์ ์ปค๋ฐํ๋ฉด ์ ๋ฉ๋๋ค! ์ ์ถ๋๋ฉด ์ฆ์ ํค๋ฅผ ๋ฌดํจํํ๊ณ ์ฌ๋ฐ๊ธ๋ฐ์ผ์ธ์.
์ฃผ์ API ํค
Sonamu์์ ์์ฃผ ์ฌ์ฉํ๋ ์ธ๋ถ ์๋น์ค API ํค์
๋๋ค.
AWS (S3 ์คํ ๋ฆฌ์ง)
# 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 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
SendGrid
Twilio
Firebase
# Stripe ๊ฒฐ์
STRIPE_SECRET_KEY=your-stripe-secret-key
STRIPE_PUBLISHABLE_KEY=your-stripe-publishable-key
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
# SendGrid ์ด๋ฉ์ผ
SENDGRID_API_KEY=your-sendgrid-api-key
SENDGRID_FROM_EMAIL=[email protected]
# Twilio SMS
TWILIO_ACCOUNT_SID=your-twilio-account-sid
TWILIO_AUTH_TOKEN=your-twilio-auth-token
TWILIO_PHONE_NUMBER=+1234567890
# Firebase
FIREBASE_API_KEY=your-firebase-api-key
FIREBASE_PROJECT_ID=my-project
FIREBASE_APP_ID=your-firebase-app-id
ํ๊ฒฝ๋ณ ํค ๊ด๋ฆฌ
์ ๋ ํ๋ก๋์
ํค๋ฅผ ๊ฐ๋ฐ ํ๊ฒฝ์ ์ฌ์ฉํ์ง ๋ง์ธ์! ๊ฐ ํ๊ฒฝ๋ง๋ค ๋ณ๋์ ํค๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
๊ฐ๋ฐ
์คํ
์ด์ง
ํ๋ก๋์
# ํ
์คํธ์ฉ ํค ๋๋ ์๋๋ฐ์ค ํค
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
ํน์ง:
- ํ
์คํธ ๋ชจ๋ ํค ์ฌ์ฉ
- ์๊ธ ๋ถ๊ณผ ์๊ฑฐ๋ ์ต์ํ
- ์ ํ๋ ๊ถํ
# ์คํ
์ด์ง ์ ์ฉ ํค
OPENAI_API_KEY=your-staging-openai-key
AWS_ACCESS_KEY_ID=your-staging-aws-key-id
AWS_SECRET_ACCESS_KEY=your-staging-aws-secret-key
S3_BUCKET=myproject-staging
STRIPE_SECRET_KEY=your-test-stripe-key # ์ฌ์ ํ ํ
์คํธ ๋ชจ๋
ํน์ง:
- ํ๋ก๋์
๊ณผ ๋ถ๋ฆฌ๋ ๋ฆฌ์์ค
- ์ค์ ์๊ธ ๋ฐ์ ๊ฐ๋ฅ
- ํ๋ก๋์
๊ณผ ๋์ผํ ๊ถํ
# ํ๋ก๋์
ํค (๋งค์ฐ ์๊ฒฉํ๊ฒ ๊ด๋ฆฌ)
OPENAI_API_KEY=your-production-openai-key
AWS_ACCESS_KEY_ID=your-production-aws-key-id
AWS_SECRET_ACCESS_KEY=your-production-aws-secret-key
S3_BUCKET=myproject-production
STRIPE_SECRET_KEY=your-live-stripe-key # ๋ผ์ด๋ธ ๋ชจ๋
ํน์ง:
- ์ต๊ณ ์์ค์ ๋ณด์
- ์ต์ ๊ถํ ์์น ์ ์ฉ
- ์ฃผ๊ธฐ์ ์ธ ๋กํ
์ด์
AWS IAM ์ฌ์ฉ์ ์์ฑ
์ฌ์ฉ์ ์์ฑ
- Users โ Add users
- ์ฌ์ฉ์ ์ด๋ฆ:
sonamu-s3-user
- Access type: Programmatic access ์ ํ
๊ถํ ์ค์
Option 1: ๊ธฐ์กด ์ ์ฑ
์ฐ๊ฒฐAmazonS3FullAccess โ (๋๋ฌด ๊ด๋ฒ์)
Option 2: ์ปค์คํ
์ ์ฑ
(๊ถ์ฅ){
"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 ํค ๋ฐ๊ธ
API ํค ์์ฑ
- API keys โ Create new secret key
- ์ด๋ฆ:
sonamu-production
- ๊ถํ: All ๋๋ Restricted (๊ถ์ฅ)
์ฌ์ฉ ํ๋ ์ค์
Settings โ Billing โ Usage limits ์ค์ Hard limit: $100/month (์ด๊ณผ ์ ์๋ ์ฐจ๋จ)
Soft limit: $80/month (์๋ฆผ)
์ฌ์ฉ ํ๋๋ฅผ ์ค์ ํ์ง ์์ผ๋ฉด ์์์น ๋ชปํ ์๊ธ์ด ์ฒญ๊ตฌ๋ ์ ์์ต๋๋ค!
ํค ์ ์ฅ
OPENAI_API_KEY=your-openai-api-key
๋ฐ๊ธ ์ฆ์ ์์ ํ ๊ณณ์ ์ ์ฅ (๋ค์ ํ์ธ ๋ถ๊ฐ)
๋ณด์ ๋ชจ๋ฒ ์ฌ๋ก
์ ๊ธฐ์ ์ธ ํค ๊ต์ฒด
๊ถ์ฅ ์ฃผ๊ธฐ:
- ํ๋ก๋์
: 3๊ฐ์๋ง๋ค
- ์คํ
์ด์ง: 6๊ฐ์๋ง๋ค
- ๊ฐ๋ฐ: 1๋
๋ง๋ค ๋๋ ํ์ ์
๋กํ
์ด์
์ ์ฐจ
์ ํค ์์ฑ
๊ธฐ์กด ํค๋ฅผ ์ ์งํ ์ฑ ์ ํค๋ฅผ ์์ฑํฉ๋๋ค.
์ ํค ๋ฐฐํฌ
# ์ ํค๋ฅผ ํ๊ฒฝ๋ณ์์ ์ถ๊ฐ
AWS_ACCESS_KEY_ID=your-new-aws-key-id
AWS_SECRET_ACCESS_KEY=your-new-aws-secret-key
# ์ ํ๋ฆฌ์ผ์ด์
์ฌ์์
๋ชจ๋ํฐ๋ง
24์๊ฐ ๋์ ์ค๋ฅ ์์ด ์๋ํ๋์ง ํ์ธ
์ด์ ํค ์ญ์
๋ฌธ์ ์์ผ๋ฉด ์ด์ ํค ๋นํ์ฑํ/์ญ์
ํค ๋กํ
์ด์
์ ๋ค์ดํ์ ์์ด ์งํํ๋ ค๋ฉด ์ ํค์ ์ด์ ํค๋ฅผ ๋์์ ์ ์งํ๋ ๋ธ๋ฃจ-๊ทธ๋ฆฐ ๋ฐฉ์์ ์ฌ์ฉํ์ธ์.
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
์์ธ:
- ์๋ชป๋ Access Key ID
- ํค๊ฐ ์ญ์ ๋์๊ฑฐ๋ ๋นํ์ฑํ๋จ
- ํ์ดํ ์ค๋ฅ (๊ณต๋ฐฑ, ์ค๋ฐ๊ฟ)
ํด๊ฒฐ:# ํค ํ์ธ
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
๋ค์ ๋จ๊ณ