mcpField
E
encore-infrastructure

encore-infrastructure

github.com/encoredevクラウド・DevOps30331 回インストール2 日前に更新
リポジトリ

encore-infrastructure について

Encore.ts アプリケーションでインフラを宣言するためのガイドです。リソースのパッケージレベル定義を軸に、データベース、メッセージング、ストレージ、キャッシュ、シークレット、定期ジョブを扱います。

主な機能

  1. 01パッケージレベルのインフラ宣言
  2. 02PostgreSQL データベースとマイグレーション
  3. 03Pub/Sub トピックとサブスクリプション
  4. 04オブジェクトストレージと署名付き URL
  5. 05Redis キャッシュと有効期限
  6. 06シークレットと定期ジョブ

活用シーン

  1. 01Encore.ts サービスのインフラ定義を整理
  2. 02データベーススキーマのマイグレーション追加
  3. 03Pub/Sub イベントの発行と受信
  4. 04ファイル保存と一時アクセス URL の生成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/encoredev/skills --skill encore-infrastructure

インストール後、Claude Code で/encore-infrastructureを入力して読み込まれたか確認します。

詳細

Encore インフラストラクチャ宣言

手順

Encore.ts は宣言的なインフラストラクチャを使用します。コード内でリソースを定義すると、Encore がプロビジョニングを処理します。

  • ローカル(encore run)- Encore は Docker(Postgres、Redis など)でインフラストラクチャを実行します
  • 本番環境 - Encore Cloud 経由で AWS/GCP にデプロイするか、生成されたインフラストラクチャ設定を使用してセルフホストします
重要なルール

すべてのインフラストラクチャは、関数内ではなくパッケージレベル(ファイルの先頭)で宣言する必要があります。

データベース(PostgreSQL)

import { SQLDatabase } from "encore.dev/storage/sqldb";

// CORRECT: Package level
const db = new SQLDatabase("mydb", {
  migrations: "./migrations",
});

// WRONG: Inside function
async function setup() {
  const db = new SQLDatabase("mydb", { migrations: "./migrations" });
}
マイグレーション

migrations/ ディレクトリにマイグレーションを作成します。

service/
├── encore.service.ts
├── api.ts
├── db.ts
└── migrations/
    ├── 001_create_users.up.sql
    └── 002_add_email_index.up.sql

マイグレーションの命名規則: {number}_{description}.up.sql

Pub/Sub

トピック
import { Topic } from "encore.dev/pubsub";

interface OrderCreatedEvent {
  orderId: string;
  userId: string;
  total: number;
}

// Package level declaration
export const orderCreated = new Topic<OrderCreatedEvent>("order-created", {
  deliveryGuarantee: "at-least-once",
});
パブリッシュ
await orderCreated.publish({
  orderId: "123",
  userId: "user-456",
  total: 99.99,
});
サブスクリプション
import { Subscription } from "encore.dev/pubsub";

const _ = new Subscription(orderCreated, "send-confirmation-email", {
  handler: async (event) => {
    await sendEmail(event.userId, event.orderId);
  },
});
メッセージ属性

メッセージ属性として扱うべきフィールド(フィルタリング/順序付け用)には Attribute<T> を使用します。

import { Topic, Attribute } from "encore.dev/pubsub";

interface CartEvent {
  cartId: Attribute<string>;  // Used for ordering
  userId: string;
  action: "add" | "remove";
  productId: string;
}

// Ordered topic - events with same cartId delivered in order
export const cartEvents = new Topic<CartEvent>("cart-events", {
  deliveryGuarantee: "at-least-once",
  orderingAttribute: "cartId",
});
トピック参照

静的分析を維持しながら、他のコードにトピックへのアクセスを渡します。

import { Publisher } from "encore.dev/pubsub";

// Create a reference with publish permission
const publisherRef = orderCreated.ref<Publisher>();

// Use the reference
async function notifyOrder(ref: typeof publisherRef, orderId: string) {
  await ref.publish({ orderId, userId: "123", total: 99.99 });
}

Cron ジョブ

import { CronJob } from "encore.dev/cron";
import { api } from "encore.dev/api";

// The endpoint to call
export const cleanupExpiredSessions = api(
  { expose: false },
  async (): Promise<void> => {
    // Cleanup logic
  }
);

// Package level cron declaration
const _ = new CronJob("cleanup-sessions", {
  title: "Clean up expired sessions",
  schedule: "0 * * * *",  // Every hour
  endpoint: cleanupExpiredSessions,
});
スケジュール形式
形式例説明
every"1h", "30m"シンプルな間隔(24 時間を均等に分割できる必要があります)
schedule"0 9 * * 1"Cron 式(毎週月曜日の午前 9 時)

オブジェクトストレージ

import { Bucket } from "encore.dev/storage/objects";

// Package level
export const uploads = new Bucket("user-uploads", {
  versioned: false,  // Set to true to keep multiple versions of objects
});

// Public bucket (files accessible via public URL)
export const publicAssets = new Bucket("public-assets", {
  public: true,
  versioned: false,
});
操作
// Upload
const attrs = await uploads.upload("path/to/file.jpg", buffer, {
  contentType: "image/jpeg",
});

// Download
const data = await uploads.download("path/to/file.jpg");

// Check existence
const exists = await uploads.exists("path/to/file.jpg");

// Get attributes (size, content type, ETag)
const attrs = await uploads.attrs("path/to/file.jpg");

// Delete
await uploads.remove("path/to/file.jpg");

// List objects
for await (const entry of uploads.list({})) {
  console.log(entry.key, entry.size);
}

// Public URL (only for public buckets)
const url = publicAssets.publicUrl("image.jpg");
署名付き URL

バケットを公開せずにアップロード/ダウンロード用の一時的な URL を生成します。

// Signed upload URL (expires in 2 hours)
const uploadUrl = await uploads.signedUploadUrl("user-uploads/avatar.jpg", { ttl: 7200 });

// Signed download URL
const downloadUrl = await uploads.signedDownloadUrl("documents/report.pdf", { ttl: 7200 });
バケット参照

特定の権限を持つバケットへのアクセスを他のコードに渡します。

import { Uploader, Downloader } from "encore.dev/storage/objects";

// Create a reference with upload permission only
const uploaderRef = uploads.ref<Uploader>();

// Create a reference with download permission only
const downloaderRef = uploads.ref<Downloader>();

// Permission types: Downloader, Uploader, Lister, Attrser, Remover,
// SignedDownloader, SignedUploader, ReadWriter

キャッシュ(Redis)

キャッシュクラスター
import { CacheCluster } from "encore.dev/storage/cache";

// Package level
const cluster = new CacheCluster("my-cache", {
  evictionPolicy: "allkeys-lru",
});

別のサービスで定義されたクラスターを参照します。

const cluster = CacheCluster.named("my-cache");

削除ポリシー: "allkeys-lru"(デフォルト)、"noeviction"、"allkeys-lfu"、"allkeys-random"、"volatile-lru"、"volatile-lfu"、"volatile-ttl"、"volatile-random"。

キースペースの型

各キースペースにはキーの型(Redis キーの生成に使用)と値の型があります。

import {
  StringKeyspace,
  IntKeyspace,
  FloatKeyspace,
  StructKeyspace,
  StringListKeyspace,
  NumberListKeyspace,
  StringSetKeyspace,
  NumberSetKeyspace,
  expireIn,
} from "encore.dev/storage/cache";

// String values
const tokens = new StringKeyspace<{ tokenId: string }>(cluster, {
  keyPattern: "token/:tokenId",
  defaultExpiry: expireIn(3600 * 1000), // 1 hour in ms
});

await tokens.set({ tokenId: "abc" }, "value");
const val = await tokens.get({ tokenId: "abc" }); // undefined on miss
await tokens.delete({ tokenId: "abc" });

// Integer values (supports increment/decrement)
const counters = new IntKeyspace<{ userId: string }>(cluster, {
  keyPattern: "requests/:userId",
  defaultExpiry: expireIn(10 * 1000),
});

const count = await counters.increment({ userId: "user123" }, 1);
await counters.decrement({ userId: "user123" }, 1);

// Float values
const scores = new FloatKeyspace<{ oddsId: string }>(cluster, {
  keyPattern: "odds/:oddsId",
});

// Structured data (stored as JSON)
interface UserProfile {
  name: string;
  email: string;
}

const profiles = new StructKeyspace<{ userId: string }, UserProfile>(cluster, {
  keyPattern: "profile/:userId",
  defaultExpiry: expireIn(3600 * 1000),
});

await profiles.set({ userId: "123" }, { name: "Alice", email: "[email protected]" });

// Lists
const recentItems = new StringListKeyspace<{ userId: string }>(cluster, {
  keyPattern: "recent/:userId",
});

await recentItems.pushRight({ userId: "user123" }, "item1", "item2");
const items = await recentItems.getRange({ userId: "user123" }, 0, -1);

// Sets
const tags = new StringSetKeyspace<{ articleId: string }>(cluster, {
  keyPattern: "tags/:articleId",
});

await tags.add({ articleId: "post1" }, "typescript", "encore", "backend");
const hasTag = await tags.contains({ articleId: "post1" }, "typescript");
複数のフィールドを持つキーパターン
interface ResourceKey {
  userId: string;
  resourcePath: string;
}

const resourceRequests = new IntKeyspace<ResourceKey>(cluster, {
  keyPattern: "requests/:userId/:resourcePath",
  defaultExpiry: expireIn(10 * 1000),
});
有効期限オプション
import {
  expireIn,          // milliseconds
  expireInSeconds,
  expireInMinutes,
  expireInHours,
  expireDailyAt,     // specific UTC time each day
  neverExpire,
  keepTTL,           // keep existing TTL when updating
} from "encore.dev/storage/cache";
書き込みオプション
// Override default expiry
await keyspace.set(key, value, { expiry: expireInMinutes(30) });

// Keep existing TTL
await keyspace.set(key, value, { expiry: keepTTL });

// Only set if key doesn't exist (throws CacheKeyExists otherwise)
await keyspace.setIfNotExists(key, value);

// Only set if key already exists (throws CacheMiss otherwise)
await keyspace.replace(key, value);
エラー処理
import { CacheMiss, CacheKeyExists } from "encore.dev/storage/cache";

// get() returns undefined on miss (does not throw)
const value = await keyspace.get(key);

// replace() throws CacheMiss if key doesn't exist
// setIfNotExists() throws CacheKeyExists if key already exists

シークレット

import { secret } from "encore.dev/config";

// Package level
const stripeKey = secret("StripeSecretKey");

// Usage (call as function)
const key = stripeKey();

CLI 経由でシークレットを設定します:

encore secret set --type prod StripeSecretKey

ガイドライン

  • インフラストラクチャの宣言はパッケージレベルで行う必要があります
  • リソースには説明的な名前を使用してください
  • マイグレーションは連番で順序を保ってください
  • サブスクリプションハンドラーは冪等である必要があります(少なくとも 1 回の配信)
  • シークレットには、シークレットを関数として呼び出すことでアクセスします
  • Cron エンドポイントは expose: false(内部のみ)にする必要があります

コメント