SvelteKitとAWS Lambda@Edgeで始めるサーバーレスなハイブリット(SSR/SSG)ウェブページを楽々作成する


※ 当ページには【広告/PR】を含む場合があります。
2023/03/21
【nodejsアプリ開発】SvelteKitでポータブルなバイナリアプリを作れるか(しかし現状では失敗)
【nodejsシェルアプリ開発】Node.jsのSEA(Single Executable Applications)を試してみよう
蛸壺の技術ブログ|SvelteKitとAWS Lambda@Edgeで始めるサーバーレスなハイブリット(SSR/SSG)ウェブページを楽々作成する



SvelteKitでAWSにホストする感じのSSR/SSGできるハイブリッドなウェブサイトを構築するまでを解説します。
以降では以下のポイントの順当に説明してまいります。

            1. SvelteKitの開発環境の導入
2. AWSウェブサイトとして利用する際のadapter-nodeのデメリット
3. AWS向けのアダプターの使い方・実装方法
4. Serverless FrameworkでのLambda@Edgeの使い方

        

それでは早速、SvelteKitの専用アダプターによるビルドからAWSへのデプロイまでを順を追って解説していきます。


合同会社タコスキングダム|蛸壺の技術ブログ【AWS独習術】AWSをじっくり独学したい人のためのオススメ書籍&教材特集
図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書

SvelteKitの導入の始め方



SvelteKitはウェブサイトのようなページを管理するアプリ開発に特化したSvelte&Vite製のフルスタックフレームワークです。
純粋なSvelteアプリよりも少しだけ学習の難易度は高くなりますが、非常に軽量で高速なウェブサイト開発が可能になっているのが特長です。

SvelteKitプロジェクトの作成



四の五の言うより、やりながらどのようなものかを理解していったほうが早いので、早速プロジェクトを作成してみましょう。


            $ npm create svelte@latest sveltekit-app
Need to install the following packages:
  create-svelte@3.1.2
Ok to proceed? (y) y

create-svelte version 3.1.2

┌  Welcome to SvelteKit!
│
◇  Which Svelte app template?
│  Skeleton project
│
◇  Add type checking with TypeScript?
│  No
│
◇  Select additional options (use arrow keys/space bar)
│  none
│
└  Your project is ready!

Install community-maintained integrations:
  https://github.com/svelte-add/svelte-add

Next steps:
  1: cd sveltekit-app
  2: npm install (or pnpm install, etc)
  3: git init && git add -A && git commit -m "Initial commit" (optional)
  4: npm run dev -- --open

To close the dev server, hit Ctrl-C

Stuck? Visit us at https://svelte.dev/chat

        

インストール途中で、初期化オプションが聞かれると思いますが、ここでは
Skelton > No type-checking > No additional option の無し無しメニューを選択しています。

            $ cd sveltekit-app
$ yarn install
$ yarn dev
  VITE v4.1.4  ready in 587 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: http://172.22.0.2:5173/
  ➜  press h to show help
11:53:18 AM [vite-plugin-svelte] ssr compile in progress ...
11:53:18 AM [vite-plugin-svelte] ssr compile done.
package         files     time     avg
sveltekit-app       3   93.2ms  31.1ms

        

デフォルトではポート
5173 が開くので、ブラウザで http://localhost:5173 にアクセスすると、以下のようなページが起動します。

合同会社タコスキングダム|蛸壺の技術ブログ

adapter-nodeを使ったExpress-SSRサイトを構築する方法



※ デメリットを浮き彫りにするため、最初に
adapter-node を使ったビルドからお話しますが、本命のやり方ではないので、結果だけを素早く知りたいかたは 次の節 からお読みください。


nodejsアプリ用のアダプターである
「@sveltejs/adapter-node」 については以前の記事で紹介していました。

合同会社タコスキングダム|蛸壺の技術ブログ
【nodejsアプリ開発】SvelteKitでポータブルなバイナリアプリを作れるか(しかし現状では失敗)

pkgとnexeを使ってSvelteKitフレームワークからnodejsバイナリプログラムが作成可能かを検証してみます。



前回で言えば、adapter-nodeを使ってnodejsアプリをバイナリ化できるかを検証するのが趣旨だったのですが、どちらかといえばExpressなどでサーバー側のSSRサイトを作るほうがそもそもの目的です。


            $ yarn add @sveltejs/adapter-node -D

        

アダプターインストール後に、
svelte.config.js を以下のように編集します。

            //import adapter from '@sveltejs/adapter-auto';
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/kit/vite';

/** @type {import('@sveltejs/kit').Config} */
const config = {
    preprocess: [vitePreprocess()],
    kit: {
        adapter: adapter(),
    }
};

export default config;

        


ルートフォルダに
.env ファイルを新しく追加し、以下のユーザー変数を追記しましょう。

            DEV_CUSTOM_PORT=5173

        

その後、
yarn build でビルド後に仕上がるリソース(デフォルトでは build フォルダ内)には、主に index.jshandler.js の2つのサーバー用jsファイルが出力されています。

合同会社タコスキングダム|蛸壺の技術ブログ
            + index.js:
    SvelteKit標準のサーバーアプリ本体
+ handler.js:
    Express等のカスタムサーバー向けのミドルウェア

        

で使い分けることができます。
今回は
index.js をローカルで開発用に使い、本番の運用では handler.js を使ってExpressサーバー上で起動させるような使い方を目指します。

Expressサーバーとして試してみる



ものは試しで、
handler.js がExpressミドルウェアとして機能するかを試してみましょう。
まずはExpressを導入します。

            $ yarn add express
$ yarn add @types/express -D

        

サーバー本体となるJSファイルを作成しましょう。

            $ touch server.js

        


このファイルの中身を簡単に試してみます。

            import express from "express";
import { handler } from './build/handler.js';

const port = 5173;
const endpoint = `http://localhost:${port}`;

const app = express();

app.use(handler);

app.listen(port, () => {
    process.stdout.write(`\x1b[0;32m\x1b[6m👇をCtrl+クリックしてブラウザで開こう!\x1b[0m\n\x1b[0;43;1;37m${endpoint}\x1b[0m`);
});

        

これで
http://localhost:5173 にアクセスすると、簡単にExpressサーバーとして起動することが出来ます。
結論から言うと、こうなってしまうと
SvelteKitではなくて単なる「Svelte&Expressアプリ」 ですので、ここからウェブサイトを構築する場合にはSvelteKitの話から大きく逸脱しれしまうでしょう。
大人の事情で、どうしても典型的なExpressウェブサイトとして構築しなくてはならない場合には、以下のように
「Vite」 が用意している SSRレンダリング の技術的なシナリオを踏襲する必要があります。

参考|サーバサイドレンダリング - Vite

このViteベースのSSR方式を採用する場合、Expressミドルウェアによる独自ハイドレーションの実装が必要になり、開発者側の負担が大きくなるデメリットがあります。
では、もっと"Express寄り"のやり方として、
「Express View Engine」 を利用するという方法もあるでしょう。
Svelte用のView Engineは例えば以下のようなものです。

参考|svelte-view-engine

ただしこちらも、Svelteのビルドリソースには使えても、既にViteで低レベルな独自コードで出力されているSvelteKitのビルドリソースにはそのまま使えないため、「View Engineを使うならSvelteKitを最初から使うな」と言われれているようなものです。
このような理由から、SvelteKitでSSRウェブページを作ると決めた時点で、
デプロイ先に合わせた専用のAdapter を最初から採用すべきなのです。


合同会社タコスキングダム|蛸壺の技術ブログ【AWS独習術】AWSをじっくり独学したい人のためのオススメ書籍&教材特集
図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書

AWS向けのAdapterでSvelteKit-SSRサイトを構築する方法



この節からが本題です。
数ある
SvelteKitのAdapter ですが、残念ながらAWSは現在のところ公式のデプロイ先のターゲットとしてサポートされていません。
ただし、公式のお墨付き(?)なのかは分かりませんが、以下のアダプターを参考に自作できると紹介されています。

MikeBild / sveltekit-adapter-aws

おそらくこのサンプルを見ても、
AWS-CDK に慣れていないと、何がどうデプロイされるのか理解するはとても一筋縄にはいかないでしょう。
また、この程度のサーバーレスアーキテクチャであれば、
AWS-CDK よりも 「Serverless Framework」 のほうがより少ない設定で同様のスタックを効率よく設計することができると思います。
同様のアーキテクチャを「Serverless Framework」的に解説されている技術記事がZennで公開されており、こちらもとても参考になります。


参考|SvelteKit の SSR を AWS 環境でサーバーレスに動かす

ただし、Svelte準拠の認証ライブラリ・「sk-auth」を含む内容ですので、より実践的な中級者向けの記事になっています。
本記事では、もう少しハードルが低い、以下のAWS Serverless Adapoterを使ったSSRサイトの構築を簡単に紹介します。

yarbsemaj / sveltekit-adapter-lambda

ここで紹介されているサーバーレス設計図は以下のような模式図になっています。

合同会社タコスキングダム|蛸壺の技術ブログ


ここでのポイントはリソースごとにアクセスさせるURLオリジンを
「Lambda@Edge」 を使って振り分けていることにあります。
Lambda@Edgeを使って同じようなことを以前以下の記事でも紹介したので興味があれば一読ください。


合同会社タコスキングダム|蛸壺の技術ブログ
【Lambda@Edge x ウェブサイト運用】AWS S3&CloudFrontで構築したウェブサイトでOGP対応をしてみた

AWS S3とCloudFrontで構築したウェブサイトで、SNSのbotからOGPグラフを正しく読み込めないときの対処法を検討します。



Lambda@Edgeを使うことで、CloudFrontへ複数のキャッシュビヘイビアをオリジンごとに細かく設定しなくても良くなります。
ただし、サーバーレス設計をスッキリさせることができる一方で、Lambda@Edgeを使うには少し面倒かもしれない制約が付きます。

            1. Lambda@Edge用にIAM:UpdateAssumeRolePolicyポリシーの追加が必要
2. 基本的にus-east-1リージョン限定
3. スタックの削除の手続きが他より多い

        

ではこのAWS用のアダプターを使って実際にデプロイするまで一通りやっていきます。

SvelteKit用の専用アダプターでクライアント/サーバー側のリソースをビルド



パッケージのGitHubプロジェクトを見ながらアダプターを写本して、後でカスタマイズしやすくしても良いのですが、最初は基本的なアダプターの使い方だけを確認してみます。
まずはアダプターをインストールしましょう。

            $ yarn add @yarbsemaj/adapter-lambda -D

        

インストールしたら、
svelte.config.js にアダプターをセットします。

            import { vitePreprocess } from '@sveltejs/kit/vite';
//👇追加
import serverless from '@yarbsemaj/adapter-lambda';

/** @type {import('@sveltejs/kit').Config} */
const config = {
    preprocess: [vitePreprocess()],
    kit: {
        //👇置き換え
        adapter: serverless(),
        //...
    }
};

export default config;

        

これで準備は完了です...なんて簡単!
あとはいつもどおり、
vite build するだけで、AWS Lambdaへそのままデプロイして動作するリソースが build フォルダに払い出されます。

合同会社タコスキングダム|蛸壺の技術ブログ


ビルド後に
build フォルダ以下にいくつかのフォルダがViteによって創出されていることが分かります。
この内、AWSへデプロイされるときに必要なフォルダは以下の通りです。

            - server:
    Lambda(SSR用)にセットされるハンドラ関数(server.js)
- edge:
    Lambda@Edge(ルート仕分け)に使われるハンドラ(router.js)
- assets:
    S3バケットに保管されるクライアントへ提供する静的アセット
- prerendered
    プリレンダリングしたページが存在する場合、
    S3バケットにindex.html等が保管され、router.jsからクライアントに配給される

        

デプロイ時に、これら4つのフォルダは適切にCloudFormationの書式で振り分けないといけません。

Serverless Frameworkによるデプロイメント



デプロイ方法にも色々と選択肢が多いですが、AWSでサーバーレスアーキテクチャを組むのには定番の
「Serverless Framework」 を使います。
まずは「Serverless Framework」本体をインストールします。

            $ yarn add serverless -D

        

S3バケットの操作とLambda@Edgeに必要な2つのプラグインも導入します。

            $ yarn add @silvermine/serverless-plugin-cloudfront-lambda-edge -D -E
$ yarn add serverless-s3-deploy -D

        

また、Lambda@Edgeを扱う場合、開発者のAWSロールに
iam:UpdateAssumeRolePolicy が要求されます。
このポリシーが無い場合には、ご自分でルートユーザーに入ってIAMユーザーにポリシーを付与するか、管理者へ問い合わせましょう。

合同会社タコスキングダム|蛸壺の技術ブログ


あとはデプロイするとAWS上で動作するとは思いますが、せっかくですのでオリジナルの
serverless.yml の内容について少し解説しておきましょう。


            service: 'sveltekit-app'

frameworkVersion: "3"

plugins:
  - '@silvermine/serverless-plugin-cloudfront-lambda-edge'
  - serverless-s3-deploy

provider:
  name: aws
  runtime: nodejs16.x
  lambdaHashingVersion: 20201221
  #👇Lambda@Edgeを使う場合、"us-east-1"が必須
  region: us-east-1
  stage: ${opt:stage, 'dev'}

#👇①(後述)
package:
  individually: true
  exclude:
    - ./**
  include:
    - build/server/**
    - build/edge/**

#👇②(後述)
custom:
  assets:
    auto: true
    targets:
      - bucket:
          Ref: StaticAssets
        files:
          - source: ./build/assets/
            globs:
              - '**'
            empty: true
            headers:
              CacheControl: max-age=31104000
          - source: ./build/prerendered/
            globs:
              - '**'
            empty: true
            headers:
              CacheControl: max-age=60

#👇③(後述)
functions:
  svelte:
    handler: build/server/serverless.handler
    memorySize: 256
    timeout: 15
    url: true

  cfLambda:
    handler: build/edge/router.handler
    memorySize: 128
    timeout: 1
    lambdaAtEdge:
      distribution: 'WebsiteDistribution'
      eventType: origin-request

resources:
  Resources:
    StaticAssets:
      Type: AWS::S3::Bucket
      Properties:
        AccessControl: PublicRead
        BucketName: ${self:provider.stage}-${self:service}-static-assets

    StaticAssetsS3BucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket:
          Ref: StaticAssets
        PolicyDocument:
          Statement:
            - Sid: PublicReadGetObject
              Effect: Allow
              Principal: "*"
              Action:
                - s3:GetObject
              Resource:
                Fn::Join: ["", ["arn:aws:s3:::", { "Ref": "StaticAssets" }, "/*"]]

    #👇④(後述)
    WebsiteDistribution:
      Type: 'AWS::CloudFront::Distribution'
      Properties:
        DistributionConfig:
          Origins:
            -
              DomainName: !Select [2, !Split ["/", !GetAtt ["SvelteLambdaFunctionUrl", "FunctionUrl"]]]
              Id: default
              OriginCustomHeaders:
                -
                  HeaderName: 's3-host'
                  HeaderValue: '${self:provider.stage}-${self:service}-static-assets.s3.amazonaws.com'
              CustomOriginConfig:
                HTTPPort: 80
                HTTPSPort: 443
                OriginProtocolPolicy: 'https-only'
          Enabled: true
          Comment: '${self:service}_${self:provider.stage}'
          DefaultCacheBehavior:
            TargetOriginId: default
            Compress: true
            AllowedMethods:
              - DELETE
              - GET
              - HEAD
              - OPTIONS
              - PATCH
              - POST
              - PUT
            CachedMethods:
              - GET
              - HEAD
              - OPTIONS
            ForwardedValues:
              Cookies:
                Forward: all
              QueryString: True
            ViewerProtocolPolicy: 'redirect-to-https'

        


上記の
の箇所において、2つのLambda関数用にリソースを固めるところを見てみましょう。

            #...
package:
  individually: true
  exclude:
    - ./**
  include:
    - build/server/**
    - build/edge/**

        

SSR用の通常のLambda用に
build/server のフォルダの中身を、ルーター用のLambda@Edge用に build/edge のフォルダの中身をそれぞれ個別(= individually: true )にzipしています。
なお、SLS V3になってからだと、この
package タグの書式は非推奨です。
詳しくは下の記事をご覧ください。

合同会社タコスキングダム|蛸壺の技術ブログ
【Serveless Framework V.3対応】serverless.ymlのpackageを利用してzipにまとめるファイルを選択する方法

Serverless FrameworkからAWS Lambdaへデプロイする時に固めるzipファイルの中身を指定するserverless.ymlのpackageオプションの使い方を紹介




次に
では serverless-s3-deploy プラグインを使ったS3バケットのカスタム操作を記述しています。

            #...
custom:
  assets:
    auto: true
    targets:
      - bucket:
          Ref: StaticAssets
        files:
          - source: ./build/assets/
            globs:
              - '**'
            empty: true
            headers:
              CacheControl: max-age=31104000
          - source: ./build/prerendered/
            globs:
              - '**'
            empty: true
            headers:
              CacheControl: max-age=60

#...中略

resources:
  Resources:
    StaticAssets:
      Type: AWS::S3::Bucket
      Properties:
        AccessControl: PublicRead
        BucketName: ${self:provider.stage}-${self:service}-static-assets
#...

        

リソースとして定義した
StaticAssets をターゲットのS3バケットとして、 build/assets フォルダと build/prerendered フォルダの中身を全部アップロードしてくれます。
デプロイするたびにリソースは空(=
empty: true )にします。
なお、クライアントのブラウザへファイルのキャッシュ時間を伝える
CacheControl ヘッダの設定も適当な長さで付けてくれているようです。
目安として静的アセットファイルは
31104000秒 = 360日 、プリレンダリングのページ(index.html等)は 60秒 = 1分 です。

の部分は、Lambdaの設定です。

            #...
functions:
  svelte:
    handler: build/server/serverless.handler
    memorySize: 256
    timeout: 15
    url: true

  cfLambda:
    handler: build/edge/router.handler
    memorySize: 128
    timeout: 1
    lambdaAtEdge:
      distribution: 'WebsiteDistribution'
      eventType: origin-request
#...

        

通常のLambda関数でSSRする方を
svelte 、リクエストされたオリジンを読み取って後段に回すルーターのLambda@Edge関数を cfLambda とおいています。
ハンドラは、それぞれ、
build/server/serverless.handlerbuild/edge/router.handler を指定していることに着目してください。

svelte 側の設定ですが、アプリの規模に応じて memorySizetimeout を適宜調整します。 個人的な感覚で、メモリサイズが256MBで、タイムアウト時間が15秒もあれば十分な気がします。
また言い忘れましたが、暗黙的な運用として、Viteビルドしたリソースで作成したAWS用のハンドラは、
RestAPIでなくHttpAPI として扱われます。
つまり、「API Gateway v1 (RestAPI)」では動作しないため、必ず
「API Gateway v2 (HttpAPI)」 でLambdaが結びついているかを確認することが必要です。
このため、明示にHttpAPIであることをLambdaに伝えるためには
url: true を使うのが一番手っ取り早いです。

参考|Lambda Function URLs


またここでのLambda@Edge・
cfLambda 側の設定は、 @silvermine/serverless-plugin-cloudfront-lambda-edge プラグインで行います。
このプラグインが無くてもLambda@Edgeの設定自体は可能ですが、自作すると色々と煩わしい処理もあるので、もろもろを
lambdaAtEdge というタグでいい感じに押し込めてくれるプラグイン任せにします。
詳しくはプラグインの使い方ガイドをご覧ください。

参考|Serverless Plugin: Support CloudFront Lambda@Edge

今回のLambda@Edgeの使い方では「オリジン・リクエスト」(=
eventType: origin-request )のイベントタイプを使います。
Lambda@edgeには4つのトリガーイベントタイプがあります。 具体的な利用例は以前紹介したブログを参考にしてください。

参考|AWS S3で静的ホスティングしたウェブサイトをレスポンス400~500番台の時にエラーページへ誘導する


では最後に
の部分のCloudFrontのディストリビューションの設定を見ていきます。

            #...
WebsiteDistribution:
  Type: 'AWS::CloudFront::Distribution'
    Properties:
      DistributionConfig:
        Origins:
          -
            DomainName: !Select [2, !Split ["/", !GetAtt ["SvelteLambdaFunctionUrl", "FunctionUrl"]]]
            Id: default
            OriginCustomHeaders:
              -
                HeaderName: 's3-host'
                HeaderValue: '${self:provider.stage}-${self:service}-static-assets.s3.amazonaws.com'
            CustomOriginConfig:
              HTTPPort: 80
              HTTPSPort: 443
            OriginProtocolPolicy: 'https-only'

        

このCloudFrontディストリビューションは
「WebsiteDistribution」 としてリソースに定義されています。
キャッシュビヘイビアなどの設定はスタンダードなものですので説明は割愛し、オリジン(Origins)の設定に注目しましょう。
ここでのCloudFrontのオリジンには、デフォルトで、SSR用のLambda・
「svelte」 のFunction URLを DomainName に指定します。
LambdaのURLですが動的に変わるので、CloudFormationの組込み関数をうまく使って、
!Select [2, !Split ["/", !GetAtt ["SvelteLambdaFunctionUrl", "FunctionUrl"]]] で引き出しています。
まず
「WebsiteDistribution」 に送られたリクエストはLambda@Edge・ 「cfLambda」 が一旦盗み見て、マニュフェストファイルに該当のあったリソースがあった場合、ユーザー定義されたカスタムヘッダーに記述したS3バケットのアドレスに置き換えられます。


            #...
OriginCustomHeaders:
  -
    HeaderName: 's3-host'
    HeaderValue: '${self:provider.stage}-${self:service}-static-assets.s3.amazonaws.com'

        

という部分で
s3-host というユーザー定義のヘッダーからS3バケットのアドレス値が読み取ることができます。
余談ですが、CloudFrontのカスタムヘッダーに記述したくない場合には、S3バケットのアドレス情報を記した
.env を作ってLambda@edgeのハンドラ・ router.js で使える用にViteビルド時にコードに埋め込んでおくのも良いでしょう。
ただし、セキュリティ上の観点から
.env ファイルをZIPで固めて直接Lambda@Edgeから呼び出すのはよろしくないので、プロジェクトのプレビルド時に変数の値を解決させるような方法をとるのがベストです。

SvelteKitアプリのデプロイ



ここまでで問題がなければ、

            $ npx sls deploy --verbose

        

でAWSへのデプロイが可能になります。
デプロイ後に生成されたCloudFrontのドメインにアクセスしてみましょう。

合同会社タコスキングダム|蛸壺の技術ブログ


ウェブページが見れていたら成功です。


合同会社タコスキングダム|蛸壺の技術ブログ【AWS独習術】AWSをじっくり独学したい人のためのオススメ書籍&教材特集
図解即戦力 Amazon Web Servicesのしくみと技術がこれ1冊でしっかりわかる教科書

まとめ



今回は、SvelteKitをAWSサーバーレスアーキテクチャでハイブリッドなウェブサイトサービスをデプロイさせる方法を解説しました。
以上の記事の主な要点をまとめると、

            1. SvelteKitの開発環境の導入
2. AWSウェブサイトとして利用する際のadapter-nodeのデメリット
3. AWS向けのアダプターの使い方・実装方法
4. Serverless FrameworkでのLambda@Edgeの使い方

        

となります。
少しボリューム感のある記事内容でしたが、ひとつひとつ噛み砕きながら勉強してみてください。