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

388 lines
10 KiB
Markdown

# GPU Sync Integration Example
This document shows how to integrate GPU instance syncing into the existing sync service.
## Modified Sync Flow
```typescript
// 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:
```typescript
// 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
```typescript
// 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
```typescript
const gpuTypes = await repos.gpuInstances.getAvailableGpuTypes();
// Returns: ['NVIDIA A100', 'NVIDIA RTX6000', 'NVIDIA V100']
```
### Search for Specific GPU Configuration
```typescript
const instances = await repos.gpuInstances.search({
providerId: linodeProviderId,
minGpuCount: 2,
gpuType: 'NVIDIA A100',
minMemoryMb: 65536 // 64GB RAM minimum
});
```
### Get GPU Pricing for Region
```typescript
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
```typescript
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)
```typescript
// 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
```typescript
// 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
```bash
# Local
npm run db:init
# Remote (Production)
npm run db:init:remote
```
### Data Validation Query
```sql
-- 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)
```sql
-- 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;
```