Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/dashboard-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Dashboard Pages

# 대시보드 정적 페이지(assets/dashboard)를 GitHub Pages(spot.roboco.io)로 배포한다.
# data.json은 수집기 스택의 CloudFront가 계속 제공하며, 페이지는 이를 교차 출처로 fetch한다.
on:
push:
branches: [main]
paths: ['assets/dashboard/**', '.github/workflows/dashboard-pages.yml']
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: assets/dashboard
- id: deployment
uses: actions/deploy-pages@v4
2 changes: 1 addition & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ yamlでイメージを切り替える。プレフィックスキャッシング

robocoが常時運用している**GPUスポット確保可能性(プレイスメントスコア) x 価格の公開サービス**:

- **ダッシュボード**: https://d16jdvzof4zpo7.cloudfront.net — p5・g6eの主要タイプの
- **ダッシュボード**: https://spot.roboco.io — p5・g6eの主要タイプの
リージョン/AZ別プレイスメントスコア推移、スポット価格、曜日×時間帯ヒートマップ、
コストパフォーマンスランキング(1時間おきに更新、90日分の履歴)
- **データフィード**: https://d16jdvzof4zpo7.cloudfront.net/data.json — CORS全面許可。
Expand Down
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ prefix caching이 기본이라 바이브 코딩의 반복 컨텍스트에 유리

roboco가 상시 운영하는 **GPU 스팟 확보 가능성(배치점수) × 가격 공개 서비스**:

- **대시보드**: https://d16jdvzof4zpo7.cloudfront.net — p5·g6e 주요 타입의 리전/AZ별
- **대시보드**: https://spot.roboco.io — p5·g6e 주요 타입의 리전/AZ별
배치점수 추이, 스팟 가격, 요일×시간 히트맵, 가성비 랭킹 (1시간 주기 갱신, 90일 이력)
- **데이터 피드**: https://d16jdvzof4zpo7.cloudfront.net/data.json — CORS 전면 허용.
스키마·이용법은 [docs/spot-feed.md](docs/spot-feed.md)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ repeated-context patterns typical of vibe coding.
roboco runs an always-on **GPU spot availability (placement score) x pricing public
service**:

- **Dashboard**: https://d16jdvzof4zpo7.cloudfront.net — placement-score trends by
- **Dashboard**: https://spot.roboco.io — placement-score trends by
region/AZ for the major p5 and g6e instance types, spot pricing, a day-of-week x
hour-of-day heatmap, and a value-for-money ranking (refreshed hourly, 90-day history)
- **Data feed**: https://d16jdvzof4zpo7.cloudfront.net/data.json — fully CORS-enabled.
Expand Down
4 changes: 3 additions & 1 deletion assets/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,9 @@ <h3>${t().explain2Title}</h3>
}
renderChrome();

DATA = await (await fetch('./data.json?t=' + Math.floor(Date.now()/60000), {cache:'no-store'})).json();
// 대시보드는 GitHub Pages(spot.roboco.io), 데이터 피드는 CloudFront — 절대 URL로 교차 출처 fetch (피드는 CORS 전면 허용)
const FEED_URL = 'https://d16jdvzof4zpo7.cloudfront.net/data.json';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Low · design — 데이터 피드 URL이 특정 CloudFront 배포 도메인으로 하드코딩됨

index.html의 FEED_URL이 'https://d16jdvzof4zpo7.cloudfront.net/data.json'으로 하드코딩되어 있다. 이 URL은 수집기 스택의 DashboardCdnUrl 출력값(cloudfront.distributionDomainName)과 일치할 때만 유효하다. 수집기 스택이 destroy 후 recreate되면 CloudFront 배포 도메인이 바뀌므로 대시보드 fetch가 실패한다. README 3종과 docs/spot-feed.md에도 같은 URL이 하드코딩되어 있어 일관성 유지 부담도 있다. 데이터 피드에 커스텀 도메인(spot.roboco.io/data.json)을 쓰거나, URL을 설정 가능하게 만들면 이 결합을 해소할 수 있다.

🔆 CodeSolar

DATA = await (await fetch(FEED_URL + '?t=' + Math.floor(Date.now()/60000), {cache:'no-store'})).json();
renderChrome(); // fill in DATA-dependent sub/legend now that DATA is available

// ---- 확보 안정성 순위·타일 (데이터에서 자동 계산) ----
Expand Down
5 changes: 3 additions & 2 deletions docs/spot-feed.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ roboco가 공개 운영하는 GPU 스팟 인텔리전스 피드입니다. 수집
1시간마다 [Spot Placement Score](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-placement-score.html)와
스팟 가격 히스토리를 수집·집계해 CloudFront로 제공합니다.

- **대시보드**: CloudFront 배포 URL의 `/` (스택 출력 `DashboardCdnUrl`)
- **데이터 피드**: 같은 URL의 `/data.json` — CORS 전면 허용, 캐시 5분
- **대시보드**: https://spot.roboco.io — GitHub Pages로 배포(`.github/workflows/dashboard-pages.yml`).
기존 CloudFront URL의 `/`는 이 주소로 301 리다이렉트된다.
- **데이터 피드**: CloudFront 배포 URL(스택 출력 `DashboardCdnUrl`)의 `/data.json` — CORS 전면 허용, 캐시 5분
- **커버리지**: 인스턴스 타입·리전은 `lib/spot-score-collector-stack.ts` 상단의
`INSTANCE_TYPES` / `REGIONS` 상수가 기준. 무한정 추가할 수 없음 —
`GetSpotPlacementScores`는 계정당 조회 구성 수에 한도가 있다.
Expand Down
20 changes: 20 additions & 0 deletions lib/spot-score-collector-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';

const INSTANCE_TYPES = 'p5.48xlarge,g6e.48xlarge,g6e.24xlarge,g6e.12xlarge';
const REGIONS = 'us-east-1,us-east-2,us-west-2,ap-northeast-1,ap-northeast-2';
// 대시보드 페이지 공개 주소 (GitHub Pages — .github/workflows/dashboard-pages.yml)
const DASHBOARD_URL = 'https://spot.roboco.io';

/**
* 스팟 배치 점수 수집기 — 서빙 스택과 독립적으로 상시 가동되는 별도 스택.
Expand Down Expand Up @@ -145,6 +147,20 @@ exports.handler = async () => {
minTtl: cdk.Duration.seconds(0),
maxTtl: cdk.Duration.hours(1),
});
// 대시보드 페이지는 GitHub Pages(DASHBOARD_URL)로 이전 — 기존 CloudFront URL의 페이지 경로만
// 301로 넘기고, data.json 등 나머지(구버전 CLI feedUrl 포함)는 그대로 S3에서 제공한다
const dashRedirectFn = new cloudfront.Function(this, 'DashboardRedirectFn', {
runtime: cloudfront.FunctionRuntime.JS_2_0,
code: cloudfront.FunctionCode.fromInline(`
function handler(event) {
var uri = event.request.uri;
if (uri === '/' || uri === '/index.html') {
return { statusCode: 301, statusDescription: 'Moved Permanently',
headers: { location: { value: '${DASHBOARD_URL}' } } };
}
return event.request;
}`),
});
const dashCdn = new cloudfront.Distribution(this, 'DashboardCdn', {
defaultBehavior: {
origin: new origins.HttpOrigin(dashBucket.bucketWebsiteDomainName, {
Expand All @@ -154,6 +170,9 @@ exports.handler = async () => {
cachePolicy: dashCachePolicy,
// data.json을 타 오리진에서 fetch로 쓸 수 있게 CORS 허용 (오픈 데이터 피드)
responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS,
functionAssociations: [
{ function: dashRedirectFn, eventType: cloudfront.FunctionEventType.VIEWER_REQUEST },
],
},
comment: 'token-forge spot dashboard + data feed',
});
Expand Down Expand Up @@ -183,5 +202,6 @@ exports.handler = async () => {
new cdk.CfnOutput(this, 'ScoresTableName', { value: table.tableName });
new cdk.CfnOutput(this, 'DashboardUrl', { value: dashBucket.bucketWebsiteUrl });
new cdk.CfnOutput(this, 'DashboardCdnUrl', { value: 'https://' + dashCdn.distributionDomainName });
new cdk.CfnOutput(this, 'DashboardPagesUrl', { value: DASHBOARD_URL });
}
}
16 changes: 16 additions & 0 deletions test/spot-score-collector-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ test('CloudFront가 S3 website 오리진 앞에서 HTTPS 리다이렉트 + CORS
});
});

test('기존 CloudFront 대시보드 경로(/, /index.html)는 spot.roboco.io로 301, data.json은 제외', () => {
const t = makeCollectorTemplate();
const fns = t.findResources('AWS::CloudFront::Function');
const code: string = Object.values(fns)[0].Properties.FunctionCode;
expect(code).toContain('https://spot.roboco.io');
expect(code).toContain('301');
expect(code).not.toContain('data.json');
t.hasResourceProperties('AWS::CloudFront::Distribution', {
DistributionConfig: Match.objectLike({
DefaultCacheBehavior: Match.objectLike({
FunctionAssociations: [Match.objectLike({ EventType: 'viewer-request' })],
}),
}),
});
});

test('역할에 GetSpotPlacementScores와 테이블 BatchWriteItem 권한', () => {
const t = makeCollectorTemplate();
// arrayWith는 패턴 순서를 부분수열로 요구하므로 문장별로 따로 단언한다
Expand Down
Loading