Files
cloud-server/GPU_SYNC_EXAMPLE.md
kappa a2133ae5c9 feat: KRW 가격 지원 및 GPU/G8/VPU 인스턴스 추가
## KRW 가격 기능
- pricing 테이블에 hourly_price_krw, monthly_price_krw 컬럼 추가
- 부가세 10% + 영업이익 10% + 환율 적용 (기본 1450원)
- 시간당: 1원 단위 반올림 (최소 1원)
- 월간: 100원 단위 반올림 (최소 100원)
- 환율/부가세/영업이익률 환경변수로 분리 (배포 없이 변경 가능)

## GPU/G8/VPU 인스턴스 지원
- gpu_instances, gpu_pricing 테이블 추가
- g8_instances, g8_pricing 테이블 추가
- vpu_instances, vpu_pricing 테이블 추가
- Linode/Vultr 커넥터에 GPU 동기화 로직 추가

## 환경변수 추가
- KRW_EXCHANGE_RATE: 환율 (기본 1450)
- KRW_VAT_RATE: 부가세율 (기본 1.1)
- KRW_MARKUP_RATE: 영업이익률 (기본 1.1)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 18:57:51 +09:00

10 KiB

GPU Sync Integration Example

This document shows how to integrate GPU instance syncing into the existing sync service.

Modified Sync Flow

// src/services/sync.ts

import { GpuInstanceInput, GpuPricingInput } from '../types';

// In LinodeSync or similar sync class:

async syncLinode(providerId: number): Promise<SyncResult> {
  const startTime = Date.now();

  try {
    // 1. Fetch all instance types
    const allInstances = await this.connector.fetchInstanceTypes();

    // 2. Separate GPU and regular instances
    const gpuInstances = allInstances.filter(inst => inst.gpus > 0);
    const regularInstances = allInstances.filter(inst => inst.gpus === 0);

    // 3. Normalize regular instances (existing logic)
    const normalizedRegular = regularInstances.map(inst =>
      this.connector.normalizeInstance(inst, providerId)
    );

    // 4. Normalize GPU instances (new logic)
    const normalizedGpu = gpuInstances.map(inst =>
      this.connector.normalizeGpuInstance(inst, providerId)
    );

    // 5. Store regular instances (existing)
    const regularCount = await this.repos.instances.upsertMany(
      providerId,
      normalizedRegular
    );

    // 6. Store GPU instances (new)
    const gpuCount = await this.repos.gpuInstances.upsertMany(
      providerId,
      normalizedGpu
    );

    // 7. Handle pricing
    // GPU pricing is embedded in metadata, so we need to extract it
    await this.syncGpuPricing(providerId, normalizedGpu);

    return {
      provider: 'linode',
      success: true,
      regions_synced: 0, // regions handled separately
      instances_synced: regularCount,
      gpu_instances_synced: gpuCount, // new field
      pricing_synced: 0, // pricing handled separately
      duration_ms: Date.now() - startTime
    };
  } catch (error) {
    this.logger.error('Linode sync failed', { error });
    return {
      provider: 'linode',
      success: false,
      regions_synced: 0,
      instances_synced: 0,
      gpu_instances_synced: 0,
      pricing_synced: 0,
      duration_ms: Date.now() - startTime,
      error: error instanceof Error ? error.message : 'Unknown error'
    };
  }
}

// New method to sync GPU pricing
private async syncGpuPricing(
  providerId: number,
  gpuInstances: GpuInstanceInput[]
): Promise<number> {
  const regions = await this.repos.regions.findByProvider(providerId);
  const pricingData: GpuPricingInput[] = [];

  // For each GPU instance
  for (const gpuInstance of gpuInstances) {
    // Parse metadata to get pricing
    const metadata = gpuInstance.metadata
      ? JSON.parse(gpuInstance.metadata)
      : null;

    if (!metadata || !metadata.hourly_price || !metadata.monthly_price) {
      this.logger.warn('GPU instance missing pricing in metadata', {
        instance_id: gpuInstance.instance_id
      });
      continue;
    }

    // Get DB ID for this GPU instance
    const dbInstance = await this.repos.gpuInstances.findByInstanceId(
      providerId,
      gpuInstance.instance_id
    );

    if (!dbInstance) {
      this.logger.warn('GPU instance not found in DB after upsert', {
        instance_id: gpuInstance.instance_id
      });
      continue;
    }

    // Create pricing records for all regions
    // (Linode prices are consistent across regions)
    for (const region of regions) {
      pricingData.push({
        gpu_instance_id: dbInstance.id,
        region_id: region.id,
        hourly_price: metadata.hourly_price,
        monthly_price: metadata.monthly_price,
        currency: 'USD',
        available: 1
      });
    }
  }

  // Batch insert pricing
  return await this.repos.gpuPricing.upsertMany(pricingData);
}

Alternative: Separate GPU Sync Method

If you prefer to keep GPU sync completely separate:

// src/services/sync.ts

/**
 * Sync GPU instances separately from regular instances
 */
async syncGpuInstances(providerId: number): Promise<number> {
  this.logger.info('Syncing GPU instances', { providerId });

  try {
    // Fetch all instances
    const allInstances = await this.connector.fetchInstanceTypes();

    // Filter GPU only
    const gpuInstances = allInstances.filter(inst => inst.gpus > 0);

    this.logger.info('Found GPU instances', { count: gpuInstances.length });

    if (gpuInstances.length === 0) {
      return 0;
    }

    // Normalize
    const normalized = gpuInstances.map(inst =>
      this.connector.normalizeGpuInstance(inst, providerId)
    );

    // Store
    const count = await this.repos.gpuInstances.upsertMany(
      providerId,
      normalized
    );

    // Sync pricing
    await this.syncGpuPricing(providerId, normalized);

    this.logger.info('GPU instances synced', { count });
    return count;
  } catch (error) {
    this.logger.error('GPU sync failed', {
      providerId,
      error: error instanceof Error ? error.message : 'Unknown error'
    });
    throw error;
  }
}

Usage in Scheduled Sync

// In your cron handler or sync orchestrator:

export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    const repos = new RepositoryFactory(env.DB);

    // Get Linode provider
    const provider = await repos.providers.findByName('linode');
    if (!provider) {
      throw new Error('Linode provider not found');
    }

    // Initialize connector
    const vault = new VaultClient(env.VAULT_URL, env.VAULT_TOKEN);
    const connector = new LinodeConnector(vault, env);
    await connector.initialize();

    // Sync regular instances
    await syncService.syncLinode(provider.id);

    // Sync GPU instances (if separate)
    await syncService.syncGpuInstances(provider.id);

    // Or use combined sync that handles both
  }
};

Query Examples

Find Available GPU Types

const gpuTypes = await repos.gpuInstances.getAvailableGpuTypes();
// Returns: ['NVIDIA A100', 'NVIDIA RTX6000', 'NVIDIA V100']

Search for Specific GPU Configuration

const instances = await repos.gpuInstances.search({
  providerId: linodeProviderId,
  minGpuCount: 2,
  gpuType: 'NVIDIA A100',
  minMemoryMb: 65536 // 64GB RAM minimum
});

Get GPU Pricing for Region

const gpuInstance = await repos.gpuInstances.findByInstanceId(
  providerId,
  'g1-gpu-rtx6000-1'
);

if (gpuInstance) {
  const pricing = await repos.gpuPricing.findByGpuInstance(gpuInstance.id);
  console.log('Available in regions:', pricing.length);
}

Find Cheapest GPU Option

const cheapestGpu = await repos.gpuPricing.searchByPriceRange(
  undefined, // no min
  2.0        // max $2/hour
);

// Get instance details
for (const price of cheapestGpu) {
  const instance = await repos.gpuInstances.findById(price.gpu_instance_id);
  console.log(`${instance.instance_name}: $${price.hourly_price}/hr`);
}

API Endpoint Example (Optional)

// src/routes/gpu.ts

import { Router } from 'itty-router';
import { Env } from '../types';
import { RepositoryFactory } from '../repositories';

const router = Router({ base: '/gpu' });

/**
 * GET /gpu/instances
 * Query GPU instances with filters
 */
router.get('/instances', async (request, env: Env) => {
  const url = new URL(request.url);
  const gpuType = url.searchParams.get('gpu_type');
  const minGpuCount = url.searchParams.get('min_gpu_count');
  const providerId = url.searchParams.get('provider_id');

  const repos = new RepositoryFactory(env.DB);

  const instances = await repos.gpuInstances.search({
    providerId: providerId ? parseInt(providerId) : undefined,
    gpuType: gpuType || undefined,
    minGpuCount: minGpuCount ? parseInt(minGpuCount) : undefined
  });

  return new Response(JSON.stringify({ data: instances }), {
    headers: { 'Content-Type': 'application/json' }
  });
});

/**
 * GET /gpu/types
 * Get available GPU types
 */
router.get('/types', async (request, env: Env) => {
  const repos = new RepositoryFactory(env.DB);
  const types = await repos.gpuInstances.getAvailableGpuTypes();

  return new Response(JSON.stringify({ data: types }), {
    headers: { 'Content-Type': 'application/json' }
  });
});

export default router;

Testing

Unit Test Example

// src/repositories/gpu-instances.test.ts

import { describe, it, expect, beforeEach } from 'vitest';
import { GpuInstancesRepository } from './gpu-instances';
import { createMockD1 } from '../test-utils/mock-d1';

describe('GpuInstancesRepository', () => {
  let repo: GpuInstancesRepository;
  let mockDb: D1Database;

  beforeEach(() => {
    mockDb = createMockD1();
    repo = new GpuInstancesRepository(mockDb);
  });

  it('should find GPU instances by type', async () => {
    const instances = await repo.findByGpuType('NVIDIA A100');
    expect(instances).toBeDefined();
  });

  it('should filter by minimum GPU count', async () => {
    const instances = await repo.search({ minGpuCount: 2 });
    expect(instances.every(inst => inst.gpu_count >= 2)).toBe(true);
  });
});

Migration Notes

Running the Migration

# Local
npm run db:init

# Remote (Production)
npm run db:init:remote

Data Validation Query

-- Check GPU instances were separated correctly
SELECT
  'Regular' as type,
  COUNT(*) as count,
  SUM(CASE WHEN gpu_count > 0 THEN 1 ELSE 0 END) as gpu_count_error
FROM instance_types
UNION ALL
SELECT
  'GPU' as type,
  COUNT(*) as count,
  SUM(CASE WHEN gpu_count = 0 THEN 1 ELSE 0 END) as zero_gpu_error
FROM gpu_instances;

-- Should show:
-- Regular: count > 0, gpu_count_error = 0
-- GPU: count > 0, zero_gpu_error = 0

Backfill Existing GPU Data (If Needed)

-- If you already have GPU instances in instance_types table,
-- you can migrate them:
INSERT INTO gpu_instances (
  provider_id, instance_id, instance_name, vcpu, memory_mb,
  storage_gb, transfer_tb, network_speed_gbps, gpu_count,
  gpu_type, gpu_memory_gb, metadata, created_at, updated_at
)
SELECT
  provider_id, instance_id, instance_name, vcpu, memory_mb,
  storage_gb, transfer_tb, network_speed_gbps, gpu_count,
  gpu_type, NULL as gpu_memory_gb, metadata, created_at, updated_at
FROM instance_types
WHERE gpu_count > 0;

-- Then remove from regular instances
DELETE FROM instance_types WHERE gpu_count > 0;