Does not provide fallback content when JavaScript is not available. このサイトを視聴するにはJavaScriptを有効にしてください。nuxt-serverlessを使いサーバーレスで安定した環境を作るTips | blog.potproject.net
potpro (ぽとぷろ)

potpro (ぽとぷろ)

Full-stuck engineer(Not Full-stack)

JS/PHP/Go/Docker/Nginxなど。技術または趣味よりの発信ブログです。このブログ自体も自分がnetlify/gatsbyJS/Reactで書いてます。一部の記事はgithubアカウントでコメントできます。

nuxt-serverlessを使いサーバーレスで安定した環境を作るTips

いいタイトルが思いつかない

最近Nuxt.jsとServerless Frameworkを使ってそこそこのアクセスが見込まれるであろうものをちゃんと本番までもっていったので、その知見。

Serverless Frameworkとは

Serverless Frameworkは、オープンソースのサーバーレスアーキテクチャのためのフレームワークです。

特定のサービスだけではなく、AWS/GCP/AzureなどのいろんなFaaSに対応していますが、今回はAWS(Lambda)を前提の話として書いています。

AWSの場合は、デプロイを行うsls deployコマンドを実行するとこのように動作します。

  1. パッケージ処理としてserverless.ymlというyamlで書かれた設定ファイルより、AWS CloudFormationのテンプレートの作成及びZIP圧縮したソースコードを生成。
  2. デプロイ処理として、ZIPファイルをS3にアップロード後、AWS CloudFormationを使用してデプロイを行う。

こうすることでソースコードをLambdaに配置したり、その他使用するサービスであるS3やAPI Gatewayなどの設定も一緒に管理できるようになっています。

また、インフラ設定も全てソース内で管理出来て、普通にEC2を立ててAnsible使っていろいろやったりDockerfile書いてECSよりも管理が楽かなという気がしました。良い・・・。

ちなみに、これとは別にAWS サーバーレスアプリケーションモデル (AWS SAM)というAWS公式のサーバーレス向けモデルも存在します。 こっちも割と使える感じはありましたが、現状は多分Serverless Frameworkの方が設定が豊富かつわかりやすいです。こちらはnodeで動作するのでNuxtとも相性が良い。

サーバーレスの利点

  • サーバー落ちない
  • サーバー管理不要
  • 通常のサーバーを使うアーキテクチャよりも安い(従量課金、余剰コストの削減)

サーバーレスの欠点

  • パフォーマンスチューニングが難しい
  • サーバの知識の代わりにマネジメントサービスの知識が必要
  • サービス自体が不具合等があっても対処法がない・対処が難しい

サーバを管理しなくていいとしても、パフォーマンス最適化やコスト最適化などは必要になってきます。

サーバーレスは管理不要と言われていますが、Lambdaのアップデートがあったり、古い環境は廃止されたりするので保守しなくてもOK、ではないです。

サービスの不具合等に関しては・・・まあAmazonさんを信じるしかないですね。サポートも活用しましょう。

tonyfromundefined/nuxt-serverless

NuxtとServerless Frameworkを使うためには、Nuxtを用意してServerless frameworkをインストールしてserverless.ymlを用意し、デプロイ環境とローカルで動く環境の用意・・・とまあいろいろなものが必要になります。

一から作るのはかなり大変ですので、自分はこのtonyfromundefined/nuxt-serverlessから構築しました。

このリポジトリは、nuxtをAPI Gateway/Lambdaを使ってSSRモードでサーバサイドレンダリング、静的なページはS3とhttp Proxy(API Gatewayの機能)でホスティングします。

実際のところNuxtを静的ページで運用したければS3に置いてCloudfrontで表示みたいな形でも問題は無いのですが、今回はSEOも考えてSSRモードで運用することになったのでこうなりました。

また、このリポジトリはExpressを使用しているためRest APIを書くこともできます。 VPCを設定すれば内部の別のAPIやRDS等にも接続してバックエンド処理が可能です。nuxtというよりもはや統合JavaScript開発環境のリポジトリ。

とはいえ当然、このまま使うには問題も出てきたので、基本的にこれをカスタマイズしていく形にしました。

サーバーレスアーキテクチャ構成

実際の構成はこんな感じです。

Lambdaをhttpでアクセスできるようにするため、API Gatewayを使用しています。

後は静的サーバーのためS3と、Cloudfrontという形になります。

Cloudfront以外は↑のリポジトリに最初から設定されています。ありがたいです。

カスタマイズ

VPCの設定

今回はAPIも使用するため、lambdaから内部ネットワークから接続するVPC Lambdaを使用します。

VPCの設定もserverless.ymlに記述すればOKです。同じようにセキュリティグループとサブネットの設定が必要。環境が分かれているので、環境ごとに変更すればやりやすいです。

custom:
# <略>
  vpc:
    dev:
      # 開発環境 ネットワーク
      securityGroupIds:
        - sg-XXXXXXXXXX
      subnetIds:
        - subnet-XXXXXXX
        - subnet-XXXXXXX
    prod:
      # 本番環境 ネットワーク
      securityGroupIds:
        - sg-XXXXXXXXXX
      subnetIds:
        - subnet-XXXXXXX
        - subnet-XXXXXXX
functions:
  renderer:
    name: ${self:service}-${self:custom.stackId}-${self:provider.stage}-renderer
    handler: .nuxt/dist/serverless.handler
    vpc: ${self:custom.vpc.${self:provider.stage}}

また、アクセスを許可するためVPCのロールの設定も必要です。

resources:
  Resources:
    AWSLambdaVPCAccessExecutionRole:
      Type: AWS::IAM::ManagedPolicy
      Properties:
        Description: Creating policy for vpc connetion.
        Roles:
          - {"Ref" : "IamRoleLambdaExecution"}
        PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
                - ec2:CreateNetworkInterface
                - ec2:DescribeNetworkInterfaces
                - ec2:DeleteNetworkInterface
              Resource: "*"

s3SyncにCacheControlを設定する

このリポジトリにはserverless-s3-syncというpluginを使用しており、これを使って静的ファイルをアップロードしています。

そして、このnuxtから生成される.nuxt/dist/clientの静的ファイルは、本番環境の場合はキャッシュバスティング(ファイルが変更された場合、ファイル名を変更してキャッシュを使わないようにする)が有効になっているため、キャッシュを無限に設定しても問題ありません。

ということで、CacheControl: 'public, max-age=31536000'をS3メタデータとして設定します。

これを設定することで適切にキャッシュを使ってくれるので、Cloudfrontを使った効率的なキャッシュ、トラフィック量の削減にもつながります。

custom:
  # <略>
  s3Sync:
    - bucketName: ${self:custom.buckets.ASSETS_BUCKET_NAME}
      localDir: .nuxt/dist/client
      params:
        - '*.js':
            CacheControl: 'public, max-age=31536000'
        - 'img/*.png':
            CacheControl: 'public, max-age=31536000'
        - 'img/*.svg':
            CacheControl: 'public, max-age=31536000'
        - 'img/*.jpg':
            CacheControl: 'public, max-age=31536000'
        - 'img/*.gif':
            CacheControl: 'public, max-age=31536000'

warmup(ホットスタンバイ)設定

VPC Lambdaを使用した場合、全くアクセスがない状態・実行していない状態の時に、極端にレスポンスが悪くなるという現象が存在します。

これは、内部的にコンテナが1つもなく新しくENI(ネットワークインターフェース)を生成するため、時間が掛かっているという話らしいです。 実際、開発環境では適応していなかったため、初回アクセス時は10秒ほど掛かっていました。

これを解消するため、serverless-plugin-warmupを導入して回避します。 serverless-plugin-warmupは、Lambdaに数分間隔でアクセスしてコンテナを消させないというプラグインです。

でしたが、

この前の高速化アップデートにより、ほとんど時間が掛からなくなりました。

実際、1秒くらいで返してくれるように。この1秒はコンテナの立ち上げ時間かと思います。

[発表] Lambda 関数が VPC 環境で改善されます

とはいえ、コンテナの起動には少しだけ時間が掛かることもあるので、今のところ導入はしています。

Cloudfrontを使用する

基本的に、API GatewayをEndpointにせず、Cloudfrontを使用したほうが良いです。

というのも基本的にAPI Gatewayから吐き出されたURLはステージ名がつく形になっているため、

https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/devという感じのURLになっており、

このままで使うのは実際厳しいので、別のドメイン名が必要となります。

なのでAPI Gatewayにも、カスタムドメイン名を設定できる箇所があるのですが、 これがあまり融通が利かず、設定も最小限しかありません。

しかもエッジ最適化というオプションでは、内部的にCloudfrontを使用しているみたいですが、Cloudfrontの設定は出来ないという劣化CFとなっているため、普通にCloudfrontを使った方がよいです。

Cloudfrontでやればキャッシュ設定なども可能で、トラフィックもCloudfrontの方が安いので、使わない手はありません。

Cloudfront設定:x-api-key設定

Cloudfrontを使用するときに、API Keyを設定しましょう。

このキーはx-api-keyというリクエストヘッダを付けることによって、認証が可能です。

APIキーをCloudfrontのカスタムヘッダに登録することで、 API GatewayへのアクセスをCloudfront経由に限定してくれるように設定できます。

provider:
  # <略>
  apiKeys:
  - name: ${self:service}-${self:provider.stage}-key

でも実際元URLにアクセスは来ない気もしますし、一応セキュリティ的にやっておいたほうが良いレベルではありますね。

Cloudfront設定:gzip圧縮等

このリポジトリはTODOに書いてある通り、gzip圧縮等を行ってくれません。とはいえ、gzip圧縮なんてパフォーマンス的には必須レベルでほしいところ。

実際、API Gatewayではgzip提供してくれない感じだったので、Cloudfrontの圧縮機能を使うことで圧縮可能です。ファイルが壊れたりすることもなく実際に問題なく動作しています。

負荷テスト

実際、「サーバーレスアーキテクチャって遅かったりするんじゃないの?」「大規模サイトに耐えゆるのかわからない・・・」とは思われているかと思います。

今回大量のアクセスが見込まれるということもあり、自分も大規模なサイトやアクセスに耐えゆるのかは、事例もなくわからん・・・という感じでした。

特に、lambdaに関しては「同時実行数制限」「実行時間30秒制限」「上記のVPC遅い問題(解消済み)」とまあ大規模だとハマりそうな部分がよくあったということもあり、正直不安でした。

そのため、API Gatewayに対して負荷テストを行い、問題なく耐えられるのか試してみました。

ちなみに使っている負荷テストツールはgatlingです。サクッとEC2インスタンスから大量アクセスする形となります。

なんかいいグラフが無かったので文章ですが、大体最大600req/sくらいでテスト。

  • nuxtでSSRする部分であれば平均120msくらいで返して来て安定
  • RDSに接続してデータを返す部分も平均100msくらいで問題なく動作(ただ、これに関してはまだアンチパターンが残っているので、よく負荷を高めてもっと検証が必要か?)
  • たまにスパイクアクセスが来たときは1,2秒くらいは平均1000msくらいになるが、リクエストの失敗は無し(コンテナが裏で立ち上がっている?)
  • やはりいくら負担を掛けてもほぼ速度は変わらず(負荷テスト用サーバーの方が耐えきれない件)

ということで、多分問題なし。

RDSへの接続は問題あるといわれていましたが、600req/sくらいだと全然問題なく、メトリクスを見ても全然余裕だった。これもVPC高速化で改善されているようです。でも最大同時接続数問題はまだ存在するので、この数十倍のアクセスかつ重いクエリを発行するようになるとまた結果が違ってくるのかも。

なぜAWS LambdaとRDBMSの相性が悪いかを簡単に説明する

やはりサーバーレスは予測できないような負担にはかなり効果的で、管理も楽という感じでした。

ただ、欠点としていきなりの大量アクセスに関してはこっちでコンテナの数をあらかじめ増やしておいて耐える、みたいなことはできませんので、大量のアクセスが来ることを見込んでいる場合には、コンテナ数を事前に増やせるECSやk8sの方がよいです。

後はRDS等に接続するバックエンドAPIも今のところは避けていた方が無難。

どうしてもやりたいのであればあらかじめ負担を与えといてコンテナを増やすというハックがありますが・・・いやそれはどうなんだろう

でも、コンテナの起動が裏で走り、それもかなり早いような挙動でしたので、高負荷状態は多分数秒あれば元に戻ります。そこが容認できれば全然良いと思います。

後は適切にCloudfrontも併用してキャッシュ設定をしていれば、より安定性が高まります。

後はlambda同時実行数制限に関しては、上限に達するとどうしようもなくなりエラーを返すと思うのでAWSサポートに依頼して上げといたほうが良いです。別件で既に上げていたので今回は触れませんでしたが。

というより、この前のlambdaのアップデートで、コールドスタート対策以外にも以前よりかなり安定している気がするんですよね。Amazonさん凄い。

まとめ

書いて気付きましたが、これ殆どnuxt関係なくサーバーレスアーキテクチャのサーバーサイドのパフォーマンス改善とか負荷テストの話になってますね。

これも大事なのですが、nuxt関係ない感じが強いので次はnuxtのフロントエンドパフォーマンス改善の記事でも書きます。多分。