13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

2025年2月に Serverless, Inc より Serverless Container Framework の発表 があり、6月には v2 が公開されました。

Serverless Framework は条件に当てはまる企業での利用が有料化され、以前に比べると話題として上がることも少なくなったように思われますが、Sapeetでは以前よりサーバーレス構成を利用する際によく利用しています。ただ、ここだけはECSで動かしたいんだよなぁということもあり、そのような場合にECSのインフラ構成を準備するのが結構重荷となっていました。
今年発表された Serverless Container Framework は、まさに我々が望んでいた要求を満たしてくれるように思われ、今年いくつかのプロジェクトで利用し実証を行いました。この記事ではフレームワークの概要と、実際に利用しての所感を共有します。

Serverless Container Framework のパワー

公式の説明にある通り、一番嬉しいこととしては同じDockerコンテナのデプロイ先を Lambda ↔ ECS Fargate 間で簡単に切り替えられることにあります。1

Serverless Container Framework - Introduction 00-00-09.10.png

たとえば WebAPI を Lambda で動かしスケールしやすくし、時間のかかるバッチ処理は ECS で実行(Lambda は基本的には最大15分までしか動かせない)、あるいは WebAPI を ECS で動かしながら一部処理を Lambda で実行、といったことが簡単に制御できることになります。

Lambda を利用した場合、Serverless Framework と異なり API Gateway は作成されず、Lambda Function URL 上で実行されます。
ECS 関連では必ず必要になるが毎回同じような設定を行っていた複数の関連リソースを自動的にオーソドックスな構成で作成し、カスタマイズしたい部分だけ設定を上書きすることが可能で、導入のインフラ面のハードルが非常に小さくなっています。また VPC やサブネット、セキュリティグループなどもすでに作成済みのものを参照させなければ自動的に作成されます。

CloudFront Functions によるルーティング

Sapeet ではこれまでWebサーバーの基盤として ECS ではなく Lambda を利用する場面が多かったのですが、Serverless Container Framework では当初 Lambda だけを利用した設定を行っても必ず Application Load Balancer が作成され、Lambda にも ALB 経由でトラフィックのルーティングが行われているようでした。

そんな中 この記事 が公開され、それによると

キャッシュ & エッジでのルーティング

AWS ECS, Lambda へトラフィックをルーティングするにあたり、より高速なパフォーマンスと柔軟性を得るため AWS CloudFront Functions を介したルーティングを導入しました。このアプローチでは Lambda に Function URL を経由して直接ルーティングを行うことにより、AWS Application Load Balancer を介した場合に生じてしまう、ストリーミングのサポートの欠如、リクエスト・レスポンスの1MB上限、といった重要な制限を回避することができます。

さらにコンピュートサービス間のワークロード切り替えが、ALB を利用した場合のように再設定に時間を要することなく、即座に行われます。

アーキテクチャ変更に関する覚書き

新しくデプロイを行うと、すべてのトラフィックは CloudFront Function を介してルーティングされるようになります。これにより、以下の点が導入されます。

  • コンピュートタイプやクラウドをまたいだ、より高い柔軟性
  • パフォーマンスとコストへの影響(必要に応じてテストを実施してください)
  • 今後、ルーティング関数を編集することのサポートも予定

という説明があります。最新版ではこの部分が実際どうなのか、作られたリソースをもとに考察しましょう。

今回検証用に次のような、Lambdaだけを使う設定でデプロイし確認しています。

deployment:
  type: aws@1.0

containers:
  web1:
    compute:
      type: awsLambda
    src: .
    routing:
      domain: test.example.com
      pathPattern: /*
      pathHealthCheck: /health

作成されたリソースを見ていきましょう。

CloudFront を見ると、この場合でもオリジンとして ALB と Lambda関数URL の2つが設定されています。

CloudFrontのオリジン

ビヘイビアは1つだけ定義されており、オリジンとして ALB が指定されているように見えます。

CloudFrontのビヘイビア

さらに Viewer Request に紐づけて、自動的に作成された CloudFront Functions が紐づいていました。

関数の関連付け

CloudFront Functions

併せて CloudFront の KeyValueStore にもリソースが作られています。

KeyValueStores

Valueの中身は以下のようなオブジェクトです。

{
  "paths": {
    "/*": {
      "originId": "your-app-name-dev-lambda-web1-origin",
      "originType": "lambda"
    }
  }
}

これが CloudFront Functions から参照されています。関数内部の実装は以下の通りです。

import cf from 'cloudfront'

async function handler(event) {
  // This is added at deploy time and is immutable after the function is created
  const kvName = 'your-app-name-dev:routing'
  // Get request details
  var request = event.request;
  var uri = request.uri;

  // Try to find a matching route in the KV store
  var routeData = null;
  try {
    const kvsHandle = cf.kvs()
    routeData = await kvsHandle.get(kvName, { format: "json" })
  } catch (e) {
    // KV lookup failed, fallback to default
    console.log('KV lookup error: ' + e.message);
  }

  if (routeData && routeData.paths) {
    const matchingPath = findMatchingPath(uri, routeData.paths);
    if (matchingPath) {
      updateOrigin(request, routeData.paths[matchingPath]);
    }
  }
  return request;
}

// Find the path pattern that matches the request URI
function findMatchingPath(uri, paths) {
  // First try for exact match
  if (paths[uri]) {
    return uri;
  }

  // Then check for wildcard matches
  // Sort path patterns by specificity (length) to prioritize more specific matches
  const pathPatterns = Object.keys(paths).sort((a, b) => b.length - a.length);

  for (let i = 0; i < pathPatterns.length; i++) {
    const pattern = pathPatterns[i];
    if (pattern === '/*') {
      return pattern; // Catch-all route
    }

    // Handle wildcard patterns like /api/* matching /api/users
    if (pattern.endsWith('/*')) {
      const prefix = pattern.slice(0, -1); // Remove the '*'
      if (uri.startsWith(prefix)) {
        return pattern;
      }
    }
  }

  return null; // No matching path found
}

function updateOrigin(request, pathConfig) {
  try {
    if (pathConfig.originId) {
      cf.selectRequestOriginById(pathConfig.originId);
      // Set custom headers to indicate which origin to use
      request.headers['x-origin-id'] = { value: pathConfig.originId };
      request.headers['x-origin-type'] = { value: pathConfig.originType };
      request.headers['x-forwarded-host'] = { value: request.headers['host'].value };
    }
  } catch (e) {
    console.log('Error updating origin: ' + e.message);
  }
}

読むのが面倒だったので実装の内容を Claude に解説させました。

このコードは、CloudFront のビューワーリクエスト時に動的にオリジンを切り替えるルーティング機能を実装しています。

処理フロー

  1. KVS からルーティング設定を取得
    • cf.kvs() で CloudFront KeyValueStore にアクセス
    • your-app-name-dev:routing というキーから JSON 形式のルーティング設定を読み込む
  2. リクエスト URI とパスパターンのマッチング
    • まず完全一致を試みる
    • 次にワイルドカードパターン(例: /api/*)でマッチング
    • パターンは長さ順(より具体的なものを優先)でソート
  3. オリジンの切り替え
    • マッチしたパスの設定に基づいて cf.selectRequestOriginById() でオリジンを動的に選択
    • カスタムヘッダーを付与(x-origin-id, x-origin-type, x-forwarded-host)

確かに CloudFront Functions を利用して直接 CloudFront -> Lambda Function URL へ直接ルーティング が行われ、ECS Fargate を利用する場合は ALB 経由でリクエストが処理される作りになっていました。

注意点として、Lambdaだけをデプロイ先とした場合でも、ALBは作成されアクティブになっています。

ALB

このため実際は一切トラフィックを捌かない場合でも、ALBの一定の月額固定の費用が発生してしまいます。現在のところ、不要なALBを作成しないようなオプションは見当たりませんでした。

Serverless Framework にあって Serverless Container Framework になかったもの

ライセンスキーの与え方

Serverless Framework では

licenseKey: ${ssm:/sls/license-key}

のような形で柔軟にライセンスキーの参照ができましたが、Serverless Container Framework ではこのような形で参照先が指定できないようで、次の箇所を参照し見つからなければエラーとなります。

  1. パラメータストアの /serverless-framework/license-key キー(固定)
  2. .serverlessrc ファイル
  3. 対話シェルにて入力

地味に不便です。

プラグインはない

Serverless Framework はプラグインという形で動作の拡張が可能で、重要なエコシステムを形成していましたが、Serverless Container Framework で今のところプラグインを実装も追加もできません。

resources ブロック

Serverless Framework では CloudFormation を内部的に使っていた関係でCloudFormation の宣言的な構文で必要に応じてAWSリソースを同時作成できました。Serverless Container Framework では CloudFormation を利用していないようで、resourcesブロックはサポートされておらず、必要であれば別のなにかでインフラの構成管理を行う必要があります。

もっとも Serverless Framework では重要なインフラリソースを便利な resources ブロックで定義してしまい、あとで管理に困るといったこともないわけではなかったので、初めから別に管理させられるのはある種の老婆心もあるかもしれません。

custom ブロック、特に環境差異の吸収

Serverless Framework で以下のように利用できた custom ブロックが使えません。

custom:
  customDomain:
    domainName: ${self:custom.${opt:stage}.domainName}
  dev:
    domainName: "dev.example.com"
  prod:
    domainName: "example.com"

Serverless Container Framework では、custom ブロックは消えましたが、同様に環境別の設定は stages を利用して記述が可能です(stages ブロックは Serverless Framework でも利用可能でした)。

stages:
  dev:
    params:
      domain: dev.example.com
  default:
    params:
      domain: example.com

deployment:
  type: aws@1.0

containers:
  web:
    compute:
      type: awsLambda
    src: .
    routing:
      domain: ${param:domain}
      pathPattern: /*
      pathHealthCheck: /health

現状で見えている課題

us-east-1 以外のリージョンでカスタムドメインを指定するとエラー

ドキュメントによると

containers:
  web:
    routing:
      domain: api.example.com

のように指定するだけで自動的にカスタムドメインの紐づけまでやってくれることになっています。
これは確かに us-east-1 リージョンで実行すると問題ないのですが、ap-northeast-1 のように指定して実行すると以下のようなエラーになります。

InvalidViewerCertificate: The specified SSL certificate doesn't exist, isn't in us-east-1 region, isn't valid, or doesn't include a valid certificate chain.

AWS Certificate Manager を覗くと ap-northeast-1 には証明書が作成されており、おそらくこれを CloudFront (us-east-1 で作成された証明書しか使えない) で利用しようとしてエラーになっていると推察しています。事前に us-east-1 に指定したドメインで利用可能な証明書を用意するなどしても無駄でした。

カスタマイズ可能なオプションが少ない

CloudFront に AWS WAF をアタッチする方法がなさそう

作成される ALB に arn を指定して紐づける方法は記載があります。

deployment:
  awsAlb: # Optional: Use existing WAF ACL
    wafAclArn: 'arn:aws:wafv2:us-east-1:123456789012:global/webacl/MyWebACL/12345678-1234-1234-1234-123456789012'

一方で、CloudFront の前段に AWS WAF を配置するオプションはどこにも見当たりません。

プライベートなエンドポイントの作成も現状無理そう

ALB と ECS を使って VPC 内部からのみアクセス可能なエンドポイントを作りたい、というケースあると思いますが、今のところそのようなオプションはなさそうです。

Lambda Function URL の 認証タイプ が none

CloudFront から Lambda Function URL へ直接ルーティングされますが、Lambda Function URL 側の設定で認証タイプ が NONE に設定されました。このため CloudFront を経由せず、直接関数URLへのアクセスが可能になっています。セキュリティを考えると見過ごせない箇所かもしれません。

image.png

またLambdaのリソースベースのポリシーで lambda:invokeFunction に関するステートメントがなく、警告が出ています。2

image.png

動作がかなり不安定

この他にも結構な確率でリソース作成に失敗したり、sls remove --force --all しても一部リソースがそのまま残る、エラーになって削除できないといったことが起きがちです。

Lambda関数作成失敗

この先ちゃんとメンテナンスされるのか不安

6月にバージョンアップがあった後、目新しいアップデートが行われておらず、Serverless, Inc. がちゃんと開発を続けてくれるのかという不安が付き纏います。AWS以外のクラウドへの対応も謳われていたはずですが、その後音沙汰がありません。

オープンで公開されているツールであれば、最悪メンテナンスが停止しても自分たちでなんとか対応できますが、Serverless Container Framework はソースコードは公開されておらず、もし開発が止まってしまったら他のものに乗り換えるしかありません。

今後の展望

Serverless Container Framework は本番運用で取り入れているプロジェクトもありますがまだごくわずかで、今後継続して使っていくことに問題がないか見極めようとしている段階ですが、現状だとなかなかプロダクションでの利用はかなり勇気が必要だと思います。

今後利用する中で新しく得られた知見があれば随時シェア致します。

  1. https://www.youtube.com/watch?v=KXNYemGzda4

  2. see. https://dev.classmethod.jp/articles/tsnote-lambda-function-urls-invokenfunction-permission/

13
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?