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


※ 当ページには【広告/PR】を含む場合があります。
2023/03/21
【nodejsアプリ開発】SvelteKitでポータブルなバイナリアプリを作れるか(しかし現状では失敗)
蛸壺の技術ブログ|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の使い方
        
となります。

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