๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

๋นŒ๋“œ์™€ ๋ฐฐํฌ

API ์„œ๋ฒ„ ๋นŒ๋“œ:
cd api
pnpm build
๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ์€ api/dist ๋””๋ ‰ํ„ฐ๋ฆฌ์— ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.Web ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ:
cd web
pnpm build
๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ์€ web/dist ๋””๋ ‰ํ„ฐ๋ฆฌ์— ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
API ์„œ๋ฒ„:
cd api
NODE_ENV=production node dist/index.js
๋˜๋Š” PM2 ์‚ฌ์šฉ:
pm2 start dist/index.js --name sonamu-api
Web ์„œ๋ฒ„:Nginx๋‚˜ Apache๋กœ ์ •์  ํŒŒ์ผ ์„œ๋น™:
# nginx.conf
server {
  listen 80;
  server_name example.com;

  root /var/www/sonamu/web/dist;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }

  location /api {
    proxy_pass http://localhost:1028;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }
}

ํ™˜๊ฒฝ ๋ณ€์ˆ˜

.env ํŒŒ์ผ ์ƒ์„ฑ:
# api/.env.production
NODE_ENV=production
PORT=1028

# Database
DB_HOST=prod-db.example.com
DB_PORT=5432
DB_NAME=sonamu_prod
DB_USER=postgres
DB_PASSWORD=secure_password

# Session
SESSION_SECRET=your-super-secret-key

# CORS
CORS_ORIGIN=https://example.com

# Cache (Redis)
REDIS_HOST=redis.example.com
REDIS_PORT=6379
REDIS_PASSWORD=redis_password
sonamu.config.ts์—์„œ ์‚ฌ์šฉ:
export default {
  server: {
    listen: {
      port: Number(process.env.PORT) || 1028,
    }
  },
  database: {
    connections: {
      main: {
        host: process.env.DB_HOST,
        port: Number(process.env.DB_PORT),
        database: process.env.DB_NAME,
        user: process.env.DB_USER,
        password: process.env.DB_PASSWORD,
      }
    }
  },
  session: {
    secret: process.env.SESSION_SECRET,
  }
} satisfies SonamuConfig;
๋ฐฉ๋ฒ• 1: ํ™˜๊ฒฝ ๋ณ€์ˆ˜
# .env ํŒŒ์ผ์€ .gitignore์— ์ถ”๊ฐ€
echo ".env" >> .gitignore
echo ".env.*" >> .gitignore
๋ฐฉ๋ฒ• 2: Secret ๊ด€๋ฆฌ ๋„๊ตฌ
  • AWS Secrets Manager
  • HashiCorp Vault
  • Azure Key Vault
// AWS Secrets Manager ์˜ˆ์‹œ
import { SecretsManager } from "@aws-sdk/client-secrets-manager";

const client = new SecretsManager({ region: "us-east-1" });

async function getSecret(secretName: string) {
  const response = await client.getSecretValue({ SecretId: secretName });
  return JSON.parse(response.SecretString!);
}

const dbSecret = await getSecret("prod/database");

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

1. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒํƒœ ํ™•์ธ:
NODE_ENV=production pnpm sonamu migrate status
2. Shadow DB์—์„œ ํ…Œ์ŠคํŠธ (๊ถŒ์žฅ):
NODE_ENV=production pnpm sonamu migrate shadow-test
3. ์‹ค์ œ DB์— ์ ์šฉ:
NODE_ENV=production pnpm sonamu migrate run
4. ๋กค๋ฐฑ (ํ•„์š”์‹œ):
NODE_ENV=production pnpm sonamu migrate rollback
PostgreSQL ๋ฐฑ์—…:
# ์ˆ˜๋™ ๋ฐฑ์—…
pg_dump -h prod-db.example.com -U postgres -d sonamu_prod > backup.sql

# ์••์ถ• ๋ฐฑ์—…
pg_dump -h prod-db.example.com -U postgres -d sonamu_prod | gzip > backup.sql.gz

# ๋ณต์›
gunzip < backup.sql.gz | psql -h prod-db.example.com -U postgres -d sonamu_prod
์ž๋™ ๋ฐฑ์—… (Cron):
# crontab -e
# ๋งค์ผ ์ƒˆ๋ฒฝ 2์‹œ ๋ฐฑ์—…
0 2 * * * /usr/bin/pg_dump -h prod-db.example.com -U postgres sonamu_prod | gzip > /backups/sonamu_$(date +\%Y\%m\%d).sql.gz

# 30์ผ ์ด์ƒ ๋œ ๋ฐฑ์—… ์‚ญ์ œ
0 3 * * * find /backups -name "sonamu_*.sql.gz" -mtime +30 -delete

๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…

Winston ์‚ฌ์šฉ:
// sonamu.config.ts
import winston from "winston";

export default {
  server: {
    logger: winston.createLogger({
      level: process.env.LOG_LEVEL || "info",
      format: winston.format.json(),
      transports: [
        new winston.transports.File({
          filename: "logs/error.log",
          level: "error"
        }),
        new winston.transports.File({
          filename: "logs/combined.log"
        })
      ]
    })
  }
} satisfies SonamuConfig;
๋กœ๊ทธ ์‚ฌ์šฉ:
class UserModelClass extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async createUser(params: UserSaveParams): Promise<User> {
    this.logger.info("Creating user", { email: params.email });
    
    try {
      const user = await this.save(params);
      this.logger.info("User created", { userId: user.id });
      return user;
    } catch (error) {
      this.logger.error("Failed to create user", { error, params });
      throw error;
    }
  }
}
// health.frame.ts
import { BaseFrameClass, api } from "sonamu";

class HealthFrameClass extends BaseFrameClass {
  @api({ httpMethod: "GET", noAuth: true })
  async healthCheck(): Promise<{
    status: string;
    timestamp: Date;
    uptime: number;
    database: string;
  }> {
    // DB ์—ฐ๊ฒฐ ํ™•์ธ
    let databaseStatus = "ok";
    try {
      await this.getDB("r").raw("SELECT 1");
    } catch (error) {
      databaseStatus = "error";
    }
    
    return {
      status: databaseStatus === "ok" ? "healthy" : "unhealthy",
      timestamp: new Date(),
      uptime: process.uptime(),
      database: databaseStatus
    };
  }
}

export const HealthFrame = new HealthFrameClass();
Sentry ํ†ตํ•ฉ:
// sonamu.config.ts
import * as Sentry from "@sentry/node";

if (process.env.NODE_ENV === "production") {
  Sentry.init({
    dsn: process.env.SENTRY_DSN,
    environment: process.env.NODE_ENV,
    tracesSampleRate: 1.0,
  });
}

export default {
  server: {
    lifecycle: {
      async onStart() {
        console.log("Server started");
      },
      async onError(error: Error) {
        if (process.env.NODE_ENV === "production") {
          Sentry.captureException(error);
        }
      }
    }
  }
} satisfies SonamuConfig;

์„ฑ๋Šฅ ์ตœ์ ํ™”

1. Connection Pool ์„ค์ •:
export default {
  database: {
    connections: {
      main: {
        pool: {
          min: 2,
          max: 10,
          acquireTimeoutMillis: 30000,
          idleTimeoutMillis: 30000
        }
      }
    }
  }
} satisfies SonamuConfig;
2. ์บ์‹ฑ ํ™œ์„ฑํ™”:
import { bentostore } from "bentocache";
import { redisDriver } from "bentocache/drivers/redis";

export default {
  server: {
    cache: {
      default: "redis",
      stores: {
        redis: bentostore()
          .useL1Layer(redisDriver({
            connection: {
              host: process.env.REDIS_HOST,
              port: 6379
            }
          }))
      }
    }
  }
} satisfies SonamuConfig;
3. ์••์ถ• ํ™œ์„ฑํ™”:
import compress from "@fastify/compress";

export default {
  server: {
    lifecycle: {
      async onStart(fastify) {
        await fastify.register(compress);
      }
    }
  }
} satisfies SonamuConfig;
Nginx ์„ค์ •:
server {
  # ...
  
  location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
  }
}

Docker

Dockerfile ์ƒ์„ฑ:
# api/Dockerfile
FROM node:18-alpine

WORKDIR /app

# pnpm ์„ค์น˜
RUN npm install -g pnpm

# Dependencies ๋ณต์‚ฌ ๋ฐ ์„ค์น˜
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

# ์†Œ์Šค ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# ๋นŒ๋“œ
RUN pnpm build

# ํ”„๋กœ๋•์…˜ ๋ชจ๋“œ๋กœ ์‹คํ–‰
ENV NODE_ENV=production
EXPOSE 1028

CMD ["node", "dist/index.js"]
docker-compose.yml:
version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: sonamu_prod
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  api:
    build: ./api
    environment:
      NODE_ENV: production
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: sonamu_prod
      DB_USER: postgres
      DB_PASSWORD: ${DB_PASSWORD}
      REDIS_HOST: redis
      REDIS_PORT: 6379
    ports:
      - "1028:1028"
    depends_on:
      - postgres
      - redis

  web:
    build: ./web
    ports:
      - "80:80"
    depends_on:
      - api

volumes:
  postgres_data:
์‹คํ–‰:
docker-compose up -d

CI/CD

.github/workflows/deploy.yml:
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: Install pnpm
        run: npm install -g pnpm
      
      - name: Install dependencies
        run: pnpm install
      
      - name: Run tests
        run: pnpm test
      
      - name: Build
        run: pnpm build
      
      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/sonamu
            git pull
            pnpm install
            pnpm build
            pm2 restart sonamu-api

๋ณด์•ˆ

Letโ€™s Encrypt + Nginx:
# Certbot ์„ค์น˜
sudo apt install certbot python3-certbot-nginx

# ์ธ์ฆ์„œ ๋ฐœ๊ธ‰
sudo certbot --nginx -d example.com -d www.example.com

# ์ž๋™ ๊ฐฑ์‹  ํ…Œ์ŠคํŠธ
sudo certbot renew --dry-run
Nginx HTTPS ์„ค์ •:
server {
  listen 443 ssl http2;
  server_name example.com;

  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers HIGH:!aNULL:!MD5;

  # ...
}

# HTTP to HTTPS ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
server {
  listen 80;
  server_name example.com;
  return 301 https://$server_name$request_uri;
}
// sonamu.config.ts
export default {
  server: {
    cors: {
      origin: process.env.CORS_ORIGIN || "http://localhost:3028",
      credentials: true,
      methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
    }
  }
} satisfies SonamuConfig;
import rateLimit from "@fastify/rate-limit";

export default {
  server: {
    lifecycle: {
      async onStart(fastify) {
        await fastify.register(rateLimit, {
          max: 100,  // 100 requests
          timeWindow: '1 minute'
        });
      }
    }
  }
} satisfies SonamuConfig;

Best Practices

๋ณด์•ˆ:
  • HTTPS ์„ค์ •
  • ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๋ฏผ๊ฐ ์ •๋ณด ๊ด€๋ฆฌ
  • CORS ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •
  • Rate Limiting ์„ค์ •
  • SQL Injection ๋ฐฉ์ง€ (Puri ์‚ฌ์šฉ)
  • XSS ๋ฐฉ์ง€ (์ž…๋ ฅ ๊ฒ€์ฆ)
์„ฑ๋Šฅ:
  • Connection Pool ์ตœ์ ํ™”
  • Redis ์บ์‹ฑ ํ™œ์„ฑํ™”
  • ์ •์  ํŒŒ์ผ ์บ์‹ฑ
  • DB ์ธ๋ฑ์Šค ์ตœ์ ํ™”
  • N+1 ์ฟผ๋ฆฌ ์ œ๊ฑฐ
๋ชจ๋‹ˆํ„ฐ๋ง:
  • ํ—ฌ์Šค์ฒดํฌ ์—”๋“œํฌ์ธํŠธ
  • ์—๋Ÿฌ ๋ชจ๋‹ˆํ„ฐ๋ง (Sentry)
  • ๋กœ๊ทธ ์ˆ˜์ง‘ (Winston)
  • DB ๋ฐฑ์—… ์ž๋™ํ™”
๋ฐฐํฌ:
  • ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ •
  • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ…Œ์ŠคํŠธ (Shadow DB)
  • CI/CD ํŒŒ์ดํ”„๋ผ์ธ
  • ๋กค๋ฐฑ ๊ณ„ํš

๊ด€๋ จ ๋ฌธ์„œ