GenHTTP Lambda

仕組み

C#のスニペットを書くと、それが返すものが数秒で公開URLにHTTPSでホストされます。ここでは、実際に使う順に全体を説明します。

lambdaとは

lambdaは、GenHTTPのハンドラーを返すスニペットです。プラットフォームがそれをコンパイルして読み込み、返されたものを専用のURLの下にマウントします。プロジェクトもビルドファイルもusingディレクティブも要りません。GenHTTPのモジュールはすべてインポート済みです。

return Content.From(Resource.FromString("hello"));

これで完全なlambdaです。/lambda/your-key/にデプロイすると、どのリクエストにも「hello」と返します。

スニペットはクラスではなく、ステートメントの並びです。最後に、リクエストを処理できるもの(ハンドラーか、そのビルダー)を返します。

最初のlambda

  1. 1
    lambdaを作成を押します。公開URLと編集用キーが発行されます。キーはlambdaに戻る唯一の方法なので、必ず保管してください。誰にも復元できません。
  2. 2
    管理画面が開きます。最初のバージョンとして、小さなRESTサービスがすでに書かれています。これはあくまで出発点です。
  3. 3
    編集用キーをエージェントに渡して、作りたいものを伝えます。エージェントはMCP経由で新しいバージョンを書きます。またはコードを開いて自分で書きます。チェックは何も保存せずにコンパイルし、コンパイラーの指摘をファイル名と行番号付きで表示します。
  4. 4
    デプロイを押すと、オンラインになります。それまでは外からアクセスできません。もう一度デプロイすると、オンラインでいられる期間が延びます。

管理画面

編集用リンクで開くのは、テキストボックスではなく管理画面です。ここではコードの多くをエージェントが書くので、最初に表示されるのはlambdaの状態です。サイドバーには、lambdaの情報(オンラインかどうか、URL、オンラインになるのを待っている新しいバージョンがあるときのボタン)と、各セクションが並びます。URLの変更や削除など、めったに使わない操作は、そこにある⋯メニューの中にあります。

概要
アプリが何か、オンラインかどうか、今日のリクエスト数と失敗数、最新の変更、残りの容量。
ドキュメント
アプリが何で、誰のために、なぜあるのか、そしてなぜこのように作られているのか。エージェントが書き、バージョンごとに保存されます。
変更依頼
変えたいところを書くと、このサーバーのエージェントが目の前で対応します。下書き(専用のURLを持つコピー)で変更を試し、うまく動いたら公開します。先に自分で下書きを試したいときは、完了したら公開するをオフにしてください。エージェントが扱うのはあなたのアプリだけです。アプリと関係のない依頼や、害を与えるための依頼は、理由を添えて断ります。
下書き
公開する前に試している変更。それぞれ専用のURLと専用のテスト用データで試せます。下書きを開くと、専用のコード、テスト用データ、ログがあります。このセクションは、下書きができると表示されます。
ファイル
バージョンのファイル(コードとアセット、つまりプログラムそのもの)。鍵か地球のアイコンで、一般公開されているかどうかがわかります。
データ
lambdaが実行中に保存するもの、すべてのバージョンで共有するデータベース、ワークスペース、シークレットです。それぞれ専用のタブがあります。テーブルやファイルの中身を見る、ファイルをアップロードする、シークレットを設定する、種類ごとにオン・オフを切り替えることができます。シンプル表示では、アプリが何かを保存すると表示されます。
バージョン
各バージョンの変更点と依頼内容、前のバージョンとの差分。ここからデプロイやロールバックができ、どのバージョンからでも下書きを作れます。
デプロイ履歴
いつ何がオンラインだったか、何が原因で止まったか。
統計
直近1時間または24時間のリクエスト数、失敗数、応答時間、よくアクセスされるパス。
ログ
リクエスト、出力された内容、エラーのスタックトレースをリアルタイムで。
コード
手で書くときに使います。チェックでコンパイル、保存でバージョンを作成、デプロイでオンラインに。下書きの中では、保存で下書きに保存し、下書きのURLに表示されます。Ctrl-Sで保存、F12で宣言へ移動します。
テスト
アプリを自動でテストする方法と、そのためのスクリプトとテストデータ。すべて表示でのみ使えます。

どのセクションも作りは同じです。タイトル、説明を開くⓘ、右側の操作ボタン、そして表示が複数あるときは、その下に切り替えボタンが並びます。コードでは、この切り替えボタンがファイルのタブです。すべて表示では、セクションがグループにまとめられています。人に見つけてもらう方法、変更を行う場所、プログラムとそのデータ、そして動作の様子です。

トラフィックとログはメモリ上にあり、保存するためではなく、様子を見るためのものです。サーバーが再起動するとリセットされます。バージョンとデプロイ履歴は保存されます。

「なぜ」を残す

バージョンは、コードと、任意の2つのメモでできています。仕様(ユーザーが何を、なぜ望んでいるか。できればユーザー自身の言葉で)と、変更内容(そのバージョンで何をするかを1行で)です。どちらもバージョン履歴で差分の横に表示されるので、何を変えたかの隣になぜが残ります。自分のためにも、履歴を読んでから手を加える次のエージェントのためにもなります。

POST /api/v1/lambdas/{editorKey}/versions
{
  "files": [ { "name": "lambda.cs", "code": "..." } ],
  "specification": "誰でも記帳できるゲストブック。再起動しても書き込みが消えないこと",
  "change": "書き込みをデータベースに保存し、再起動しても消えないようにする"
}

エージェントも、同じ2つの項目をwrite_codeに渡します。コードでは、保存するときに変更内容を聞かれます。どちらも任意です。長すぎても拒否はせず、仕様は4000文字、変更内容は500文字で切り詰めます。下書きも自分の2つのメモを持ち、確定してできたバージョンがそれを引き継ぎます。

ドキュメントとテスト

どのバージョンも、プログラムの隣に、自分について書かれたものを持っています。ドキュメント(アプリが何で、誰のために、なぜあるのか、そしてなぜこのように作られているのか)と、テスト(アプリが動くことを自動で確かめる方法と、そのためのスクリプトとテストデータ)です。エージェントは新しいlambdaと一緒にこれらを書き、変更のたびに最新の内容に保ちます。次にlambdaを変更するエージェントはまずこれらを読むので、アプリが何のためにあり、何が動き続けなければならないかがわかります。それは、コードだけではわかりません。

.lambda/docs/product.md
アプリが何で、誰のためのもので、利用者がそれで何をするのか、そしてその理由
.lambda/docs/decisions.md
技術的な判断と、その理由
.lambda/tests/README.md
アプリを自動でテストする方法と、テストの実行方法
.lambda/tests/…
テストが使うスクリプトとテストデータ

これらはほかと同じくバージョンのファイルで、.lambdaフォルダーにあります。履歴にはバージョンがそこで何を変えたかが表示され、ロールバックするとそのバージョンに合ったドキュメントが戻ります。下書きは専用のコピーを持ち、下書きを確定すると一緒に反映されます。コンパイルも配信もされず、バージョンのアセットの容量に含まれます。

管理画面では、ドキュメントに読むためのページが、テストにアプリのテスト方法とその隣のファイルが表示されます。バージョンは、ファイルと同じように選びます。ページはそこで編集することもでき、保存すると次のバージョンになります。シンプル表示ではドキュメントをアプリについてと呼び、アプリが何のためにあるかだけを表示します。直したいときは、エージェントに伝えてください。

エージェントとのやり取りに使っている言語で、次にアプリを変更する人(人でもエージェントでも)のために書かれます。コードの写しではなく、何のためにあり、なぜそうなのかを書くものです。

安全に変更する

バージョンは、一度保存すると変わりません。だからこそ、どのバージョンも残しておく価値があります。どれとでも比較でき、そのままの形でオンラインに戻せるからです。使われているlambdaを変えるときは、まず下書きで試しましょう。

  1. 1
    バージョンから、どのバージョンをもとにしても作れます。エージェントに作ってもらうこともできます。中身は、そのバージョンのコード、アセット、ドキュメント、テストと、lambdaのデータのコピーです。
  2. 2
    納得がいくまで何度でも変更します。コードで直接でも、エージェントに頼んでもかまいません。プレビューは専用のURL(/features/…/)で、下書き専用のテスト用データを使って動きます。lambdaの訪問者には何も見えず、下書きが書き込んだものがlambdaのデータに届くこともありません。
  3. 3
    うまくいったら公開するを押します。メモ付きで次のバージョンになり、オンラインになります。下書き(プレビューとテスト用データ)はなくなります。
POST /api/v1/lambdas/{editorKey}/features
{ "name": "ランキング" }

PUT  /api/v1/lambdas/{editorKey}/features/{feature}/files?deploy=true
POST /api/v1/lambdas/{editorKey}/features/{feature}/merge
{ "deploy": true }

複数の下書きを同時に進められます。ただし公開できるのは、最新のバージョンに追いついている下書きだけです。下書きを作ったあとに保存されたバージョンが、公開で元に戻ってしまわないようにするためです。別の下書きが先に公開されたときは、その変更を取り込んでから(エージェントに頼んでもかまいません)、下書きを最新の状態にしてください。勝手に公開されることはありません。これは意図したものです。APIでは、下書きを feature、公開することを merge と呼びます。

複数のファイル

型は、それを使うコードの下に書かなくてもかまいません。コードでファイルの横の+を押すと、新しいファイルがスニペットと同じ名前空間で一緒にコンパイルされるので、インポートしなくても参照できます。拡張子のない名前はC#として扱われます。

lambda.cs
var shelf = new Shelf();

return Inline.Create()
             .Get(() => shelf.All())
             .Post((Book book) => shelf.Add(book));
Shelf.cs
public sealed class Shelf
{
    private readonly List<Book> _books = [];

    public IEnumerable<Book> All() => _books;

    public Book Add(Book book)
    {
        _books.Add(book);
        return book;
    }
}

public record Book(string Title, string Author);

ページを配信する

ページを配信する方法は2つです。それとは別に、利用者がアップロードしたものを一緒に配信する方法がもう1つあります。

ページを1つ、インラインで書く

小さなものならこれで十分です。ページはスニペットの一部になります。

var page = Resource.FromString("""
                              <!doctype html>
                              <title>Mine</title>
                              <h1>It works</h1>
                              """)
                   .Type(new ContentType("text/html; charset=utf-8"));

return Content.From(page);

フォルダーに実際のファイルを置く

スタイルシートやスクリプトがあるなら、これがおすすめです。ファイルはC#のファイルと同じ方法で追加し、書いたとおりに配信されます。コンパイルはされません。

return Layout.Create()
             .Add("api", api)
             .Add(Assets.App("site"));

アップロードされたファイルを、データから

利用者がアップロードしたものや、lambdaが作ったもの(画像、文書など)を、アプリと一緒に配信するときに。アプリ自体のページには使いません。ページはフォルダーにファイルとして置けば、それを使うコードと一緒にバージョンで管理されます。

return Layout.Create()
             .Add("api", api)
             .Add("uploads", Workspace.Files("uploads"))
             .Add(Assets.App("site"));

フロントエンドを一歩ずつ

2つ目の方法を、最初から最後まで説明します。どのデモも、webというフォルダーからこの方法でページを配信しています。demo-crudを開くと実例が読めます。デモは読み取り専用で、編集用キーはデモの名前です。

  1. 1
    コードでファイルの横の+を押し、site/index.htmlと入力します。スラッシュを含む名前ならファイルはフォルダーに入り、拡張子のある名前ならその種類のファイルとして扱われます。
  2. 2
    同じようにsite/app.cssとsite/app.jsを追加します。フォルダーはURLの一部ではなく、配信のルートになります。そのため、ページからはhref="app.css"のように名前だけで参照します。
  3. 3
    画像やフォントなど、テキスト以外のものは、site内のファイルを開いてから、ファイルの横のアップロードボタンを押します。同じフォルダーに入ります。PNGはテキストエディターでは入力できないので、この方法で追加します。
  4. 4
    lambda.csでフォルダーを配信します:
    return Layout.Create().Add(Assets.App("site"));
  5. 5
    デプロイを押します。site/index.htmlは/で、site/app.cssは/app.cssで応答します。どのファイルにも一致しないURLにはページを返すので、自前でルーティングするフロントエンドでも、ディープリンクで再読み込みしたときにちゃんと動きます。
  6. 6
    横にAPIを追加すれば、ページから呼び出せます:
    var api = Inline.Create().Get("notes", () => notes);
    
    return Layout.Create()
                 .Add("api", api)
                 .Add(Assets.App("site"));

ファイルの2つの置き場所

lambdaはファイルを2か所に置いていて、エディターでも分けて表示します。ファイルにはバージョンのファイル(プログラム)が、データにはワークスペース(プログラムが保存しておくもの)があります。違いは誰のものかです。バージョンのファイルはそのバージョンのものです。データはlambdaのもので、すべてのバージョンが共有します。

バージョンの中データの中
中身コードとアセット(フロントエンドを含むプログラム)、そしてそのドキュメントとテストlambdaが書き込んだもの、誰かがアップロードしたもの
変わるタイミング変わらない(変更は新しいバージョンになる)何かが書き込まれた瞬間
デプロイするとまさにこのファイルがオンラインになる一切変わらない
ロールバックすると古いファイルに戻る影響なし(すべてのバージョンで共有)
下書きを作るとこのファイルのコピーから始まるデータのコピーを使う
消えるとき上限を超えたら、古いバージョンと一緒にlambdaと一緒に、またはオフにしたとき
コードからの参照名AssetsWorkspace

1か所にまとめることはできません。もしそうなら、デプロイのたびにlambdaが書き込んだものがすべて消えるか、配信するファイルを二度と削除できなくなるかのどちらかです。ランキングを保存するゲームには後者が必要で、そのゲームが配信するページには前者が必要です。だから、ページはバージョンに、ランキングはデータに置きます。

記録を保存する

記録(投稿、アカウント、注文、投票など)はデータベースに置きます。lambda専用のSQLiteデータベースで、データでオンにします。コードはDatabase.GetConnection()で接続を開き、テーブルを対応付ける独自のコンテキストを使ってEntity Framework Coreで読み書きします:

// migrations/V1__Create_notes.sql:
//   CREATE TABLE notes (id INTEGER PRIMARY KEY, text TEXT NOT NULL);

using (var connection = Database.GetConnection())
{
    new Evolve(connection) { Locations = [Assets.Root + "migrations"] }.Migrate();
}

return Inline.Create()
             .Get("notes", () =>
             {
                 using var db = new Notes(Database.GetConnection());

                 return db.Entries.OrderBy(n => n.Id).Select(n => n.Text).ToList();
             })
             .Post("notes", (NoteInput input) =>
             {
                 using var db = new Notes(Database.GetConnection());

                 db.Entries.Add(new Note { Text = input.Text });

                 return db.SaveChanges();
             });

record NoteInput(string Text);

class Note
{
    public long Id { get; set; }

    public string Text { get; set; }
}

// maps the table the migration made, on the connection it is handed
class Notes(SqliteConnection connection) : DbContext
{
    public DbSet<Note> Entries => Set<Note>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite(connection, contextOwnsConnection: true);

    protected override void OnModelCreating(ModelBuilder model)
        => model.Entity<Note>().ToTable("notes");
}

テーブルを作るのはマイグレーションです。バージョンと一緒にmigrations/に置くSQLファイルで、lambdaの起動時にEvolveが順番に適用します。どれも一度しか適用されないので、新しいバージョンで実行されるのは、新しく加わったものだけです。適用済みのマイグレーションは決して変更しないでください。テーブルを変えるときは、次のファイルを追加します。

ほかのデータと同じく、データベースはすべてのバージョンで共有され、デプロイやロールバックでは変わりません。下書きはそのコピーを使います。データでは、テーブルとその中身を見られます。シンプル表示では、これを「記録」と呼んでいます。.NETプロジェクトとしてダウンロードすると、ただのSQLiteファイルとして一緒に持ち出せます。

コンテキストは必要な場所で作って破棄し、同期的に使ってください(ToListAsyncとSaveChangesAsyncではなくToListとSaveChanges)。テーブルを作るのはマイグレーションで、Entity Frameworkではありません。demo-crudのデモが、これをすべて実践しています。

ファイルを保存する

Workspaceは、lambdaが読み書きできる非公開のディレクトリで、ファイルの置き場所です。誰かがアップロードした画像、lambdaが作る文書、読み込むモデルなどを置きます。記録はデータベースに置きます。ファイルについてわかっていること(誰が、いつアップロードしたか)も記録です。

// uploads go into a folder of their own, served as they are
Workspace.CreateFolder("photos");

return Layout.Create()
             .Add("photos", Workspace.Files("photos"))
             .Add("upload", Inline.Create().Post(async (Stream body) =>
             {
                 using var content = new MemoryStream();

                 await body.CopyToAsync(content);

                 Workspace.WriteBytes($"photos/{Guid.NewGuid():N}.jpg", content.ToArray());
             }));

ほかにもReadBytes、WriteBytes、Delete、List、CreateFolder、配信用のTree/Files/Appがあります。ファイルシステムのそれ以外の場所にはアクセスできません。

キーとパスワード

APIキー、パスワード、トークンはコードではなくシークレットに置きます。コードに書くと、すべてのバージョン、 すべてのダウンロード、履歴を読むすべての人の手に渡ります。コードはシークレットを名前で読みます。

var weather = new System.Net.Http.HttpClient();

weather.DefaultRequestHeaders.Add("X-Api-Key", Secret.Read("WEATHER_API_KEY"));

return Inline.Create()
             .Get("today", async () => await weather.GetStringAsync("https://weather.example/today"));

データでシークレットをオンにし、そこで値を設定します。一度保存した値は二度と表示されません。あなたにも、 エージェントにもです。できるのは置き換えることだけです。一覧には、コードが読んでいるのに値がまだない名前が表示され、 概要でも入力を求められます。Secret.Existsは設定されているかを返すので、なくても動くコードに使えます。 ほかのデータと同じく、シークレットはすべてのバージョンで共有され、下書きはそのコピーを使います。

シークレットは、データベースにはない鍵で暗号化して保存されます。ダウンロードしたプロジェクトでは、Secret.Read("NAME")は環境変数NAMEを読みます。値そのものはここに残ります。

WebSocket

対応しています。しかも後付けではありません。demo-gameのデモでは、プレイヤーをマッチングして、すべての対戦をサーバーで動かしています。いちばんシンプルな形は、3つのコールバックです:

var room = new ConcurrentDictionary<IReactiveConnection, string>();

var socket = Websocket.Functional()
                      .OnOpen(c => { room[c] = "someone"; return ValueTask.CompletedTask; })
                      .OnMessage(async (c, text) =>
                      {
                          foreach (var other in room.Keys)
                          {
                              await other.WritePayloadAsync(text);
                          }
                      })
                      .OnClose((c, _) => { room.TryRemove(c, out _); return ValueTask.CompletedTask; });

return Layout.Create().Add("chat", socket);

ページが受け取るだけのとき(カウント、フィード、スコアボードなど)は、サーバー送信イベントのほうが簡単です。サーバーが書き続ける1本の長いレスポンスで、ブラウザーが自動的に再接続します。 demo-liveデモは、すべての投票をこの方法で見ている全員に送ります。どちらの方法でも、サーバーが変更を押し出します。数秒ごとに問い合わせ直すページは、変更の有無にかかわらず毎回リクエストを送り、それでも反映が遅れます。

誰もが一度はつまずくポイントがあります。ブラウザーは、WebSocketのハンドシェイクにヘッダーを設定できません。ハンドラーに必要な情報はクエリで渡してconnection.Request.Header.Queryから読むか、秘密の情報なら最初のメッセージで送ってください。

できないこと

コードは共有サーバーで動くため、C#の一部の機能はコンパイル前に拒否されます。プロセスの起動、独自のソケットを開くこと、アセンブリの読み込み、ワークスペース外のファイルシステムへのアクセス、そしてこれらを回避するためのリフレクションです。タスクを await せずに .Result や .Wait() で待つことも拒否されます。リクエストはコアごとに1つのスレッドで実行され、タスクはそれを待っているスレッドそのもので完了しなければならないためです。

それ以外はすべて使えます。GenHTTPのモジュールAPIもまるごと使えます。拒否されたときは、単に失敗したとだけではなく、どの行がなぜ拒否されたかが表示されます。

まるごと持ち出す

エディターの.NETプロジェクトとしてダウンロードを使えば、すべてをまとめて持ち出せます。そのまま開けるソリューションなので、dotnet runで実行でき、手元に残しておけます。必要なのはGenHTTPパッケージだけで、コンテナーとしてビルド・実行するためのDockerfileも付いています。

スニペットはProject.csになり、Program.csがその返したものを配信します。ほかのファイルは、書いたとおりにそのまま移ります。WorkspaceとAssetsはプログラムの隣の2つのフォルダーになり、Platformフォルダーに分けて置かれます。メソッドも同じなので、コードを変える必要はありません。Secretは同じ名前の環境変数を読みます。値そのものはここに残ります。ドキュメントとテストは、docsとtestsに入って一緒に移ります。Databaseはdatabase/database.dbを開きます。ダウンロードには、アプリが保存した記録の入ったこのファイルが含まれます。

ここで何かを作る前に知っておいてほしいこと:書いたものはあなたのもので、まるごと持ち出せます。このサーバーで動かしているからといって、このサーバーに縛られることはありません。

コードを公開する

作ったものがほかの人の役に立ちそうなら、コードを公開しましょう。管理画面でオープンソースを開き、ライセンスを選んで(特に希望がなければMIT)、オンにします。コードにはオープンソースのアプリの中に専用のページができ、誰でも読んだり、スターを付けたり、どのバージョンでもダウンロードしたりできます。ダウンロードされるのは、.NETプロジェクトとしてダウンロードで手に入るのと同じプロジェクトで、ライセンスも一緒に入っています。

以前のものも含めてすべてのバージョンが、ドキュメント、テスト、それぞれの変更内容と一緒に公開されます。アプリが残しているもの(記録、保存したファイル、キーとパスワードの値)は決して公開されず、あなたが自分の言葉で依頼した内容や、誰がアプリを使っているかも公開されません。オフにするとページはなくなります。スターは、また公開するときのために残ります。

コードに書かれているものは、以前のバージョンも含めてすべて公開されます。キーやパスワードは、公開するかどうかにかかわらずコードには書かず、「データ」のキーとパスワードに入れてください。

エージェントに任せる

/mcpにMCPエンドポイントがあります。エージェントを接続すれば、エディターでできることはすべてできます。ガイドを読む、デモをまるごと読む、ファイルを書く、コンパイルする、デプロイする。中で使っているのは同じAPIです。

エージェントは作業しながら「なぜ」を残します(write_codeは仕様と変更内容を受け取ります)。デプロイしたものの様子も確認できます。read_logsは、lambdaの最近のリクエスト、出力された内容、発生した例外のスタックトレースを返します。エージェントはこれで、コードが動くと思い込むのではなく、実際に動くことを確かめます。同じものは管理画面でも見られます。エージェントは作業しながらドキュメントとテストを書き、何かを変更する前にそれを読み、下書きを確定する前に下書きのURLに対してテストを実行します。見つけてもらうためのページには、タイトル、説明文、アイコンと、リンクが共有されたときに表示されるプレビューを付けます。作成したページの最後には、GenHTTP Lambdaで作成したことを示す小さな一行を添えます。不要な場合はエージェントに伝えると、取り除いてくれます。

くわしくはこちら →