19
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SSTでサーバーレス環境を構築してみたらすごく良かった

Last updated at Posted at 2025-02-03

はじめに

SST とは、サーバーレス含むフルスタック・アプリケーションを簡単に構築できるフレームワークです。

v2ではCDKを基盤として構築されていましたが、
v3ではPulumiやTerraformを基盤とする形に移行しています。

Pulumiは、JavaScript、TypeScript、Python、Go、C#など、一般的なプログラミング言語をサポートしています。また、AWS、GCP、Azure、Kubernetesなど、150以上のクラウドプロバイダーやサービスに対応しています。

移行理由について記載された記事を見ましたが、非常に面白い記事でした。
CDKの循環参照問題とか、エラー発生時のロールバックが極端に遅い時があるとか、確かに〜!!っといった感じでした。
また、マルチクラウド戦略もあるようなので、そういった目的も鑑みた上での移行のようです。

元々気にはなっていたのですが、従来通りAWS上に環境構築できると言う点と、PulumiやTerraformまわりの設定は特に必要ないと言う点で、心おきなく環境構築にトライすることができました。

本記事でやること

本記事では、以下環境構成をSSTで作成します。

フロントエンド環境:
CloudFront + S3(Nuxt3)

バックエンド環境:
API Gateway(HTTP API)+ Lambda + DynamoDB

ドキュメントに記載されている NuxtアプリのGET STARTED を参考に手順を進めつつ、所々アレンジしていきます。

前準備

GET STARTEDを始める前に、いくつかの準備が必要です。

1. AWS Credentials

SSTはAWSアカウント内に各種リソースを作成します。
なので、GET STARTEDを始める前に、AWS Credentialsの設定をしておく必要があります。

ファイルから読むパターン

以下IAMユーザー作成時に生成されるアクセスキーやシークレットアクセスキーをローカルの設定ファイルに設定しておきます。
そうすることで、sstコマンド実行時にこの認証情報が参照されます。

~/.aws/credentials
[default]
aws_access_key_id = <YOUR_ACCESS_KEY_ID>
aws_secret_access_key = <YOUR_SECRET_ACCESS_KEY>

環境変数で渡すパターン

3つのパターンが記載されています。

  • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
  • AWS_SESSION_TOKEN
  • AWS_PROFILE

CLIの環境変数としていずれかを渡すことで認証情報が付与された状態でコマンドの実行ができます。

弊社では、

  • 案件毎・各環境毎(dev, stage, prod)にAWSアカウントを分けている
  • SSOログインを実行し、profileを指定してCLIからAWSアカウントにアクセスしている

という状況なので、今回の記事では以下のようにAWS_PROFILE環境変数を指定する形で説明します。

AWS_PROFILE=my-account npx sst dev

2. Bunのインストール

SSTは内部でBunが使われているようです。
BunがPCに入っていない場合、インストールしてください。
※Bunを入れていなかった私のPCではsstコマンドがうまく機能しませんでした。

後から知ったので私の環境では未検証ですが、sstコマンドに NO_BUN=true の環境変数をつけるとBUNなしで動作するみたいです。

GET STARTED

以下順序で説明していきます。

  • Nuxt SPAアプリを用いたフロントエンド環境をAWSに構築するフロー
  • API Gateway、Lambda、DynamoDBを用いたバックエンド環境をAWSに構築するフロー
  • 開発環境の起動
  • Secretsの扱いについて

Nuxtアプリを作成する

まずは、Nuxtのプロジェクトを新規作成します。

npx nuxi@latest init aws-nuxt
cd aws-nuxt

Init SST

1. sst init

次に、以下を実行します。

npx sst@latest init

コマンド実行時にプロバイダーの選択を求められるので、AWSを選択します。
そうすると、設定ファイルのsst.config.tsがプロジェクトのルートに作成されます。

2. npm install

その後、GET STARTED通り以下を実行しておきます。

npm install

フロントエンド環境構築

Nuxt SPAにより生成された静的ファイルをS3に格納し、CloudFrontで配信するまでのフローを記載します。

1. nuxt.config.tsの編集

GET STARTED通りpresetの記述を追加します。
また、NuxtをSPAアプリケーションとしたいので、ssr: falseとします。

nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2024-04-03',

  // 以下追記する
  ssr: false,
  nitro: {
    preset: 'aws-lambda'
  },
  
  devtools: { enabled: true }
})

2. sst.config.tsの編集

ドキュメントによると、Nuxtコンポーネントを用いる形で記載されています。
これを用いると、CloudFrontの設定において、静的ファイル専用のビヘイビアだけでなくNuxtのAPIサーバー専用のビヘイビアも追加されます。

sst.config.ts
new sst.aws.Nuxt("MyWeb");

しかし、今回Nuxtアプリはフロントエンドの静的ファイルを生成するのみの用途としたいので、StaticSiteコンポーネントを用いる形とします。

sst.config.ts
new sst.aws.StaticSite("MyWeb", {
  domain: {
    // Route53のホストゾーンを参照する
    // Route53にAレコードやCNAMEの追加をしてくれる
    // 自動でus-east-1でSSL証明書を取得し、CloudFrontに証明書を紐づけてくれる
    name: "ドメイン名"
  },
  // 全てのパスのCloudFrontキャッシュが削除されるまで待機する設定
  // https://sst.dev/docs/component/aws/static-site#invalidation
  invalidation: {
    wait: true
  },
  build: {
    command: "npm run generate",
    output: ".output/public"
  },
});

SSTではなくAWS側の話ですが、CloudFrontキャッシュ削除は1000回/月まで無料で行う事ができ、その後は$0.005/回 かかるようです。

3. Route53ホストゾーン作成

sst.config.tsに記述したドメイン名で、Route53のホストゾーン作成とNSレコードまわりの設定(ムームードメインやお名前.comなどへの名前解決設定)をしておきます。

以上で設定は完了です。
この記述だけでCloudFront、S3、Route53、ACMまわりの諸々の作成・設定を自動でやってくれます。

4. デプロイする

# stageは環境のようなもので、アプリの別のバージョン
# 例えば、開発ステージ、本番ステージ、個人ステージなど
# 今回、開発ステージをdevとし、CLIの引数に与える
AWS_PROFILE=my-account npx sst deploy --stage dev

これでフロントエンド環境の構築が完了します。

バックエンド環境構築

次に、API Gateway、Lambda、DynamoDBを用いたバックエンド環境を構築するフローを説明します。

1. API Gateway

ApiGatewayV2コンポーネントを使って、API Gateway(HTTP API)を作成します。

corsの許可設定だけしておきます。

sst.config.ts
const api = new sst.aws.ApiGatewayV2("MyApi", {
  cors: {
    allowOrigins: [
      // cors許可するドメイン名
      "ドメイン名"
    ]
  }
});

2. Lambda

API GatewayのRouteに対してLambda関数を紐づける部分はApiGatewayV2のドキュメントを見つつ、Lambda関数自体の細かい設定はFunctionのドキュメントを確認し、設定値を調整する形となります。

sst.config.ts
// GET
api.route("GET /api/get", // API GatewayのRoute指定
  {
    handler: "src/get.handler", // Lambda関数のパス.関数名
    architecture: "arm64", // 指定しなければx86_64
    logging: {
      format: "json", // CloudWatchへ送信されるログのフォーマット
    },
  }
);

// POST
api.route("POST /api/post",
  {
    handler: "src/post.handler",
    architecture: "arm64",
    logging: {
      format: "json",
    },
  },
);

Lambda関数の中身の記述は、後に説明する「Links」にて記載します。
先にDynamoDBの説明をします。

3. DynamoDB

DynamoDBドキュメントを見ながら設定を追加していきます。
※今回単純な構成で説明をしたいため、セカンダリインデックスの設定は行いません。

まずはDynamoDBのテーブルを作成します。

sst.config.ts
const table = new sst.aws.Dynamo("MyTable", {
  // codeをパーティションキーとする
  fields: {
    code: "string"
  },
  primaryIndex: { hashKey: "code" }
});

4. Links

Lambda関数は、何も設定していないとDynamoDBへアクセスする事ができません。
Lambda関数自体のポリシーの設定や、ランタイムでのDynamoDBのテーブルへの参照が必要になります。

SSTにはlinkという便利なSDKが用意されています。

これを使うことでポリシーの設定や、ランタイムでのAWSリソースへの参照が容易になります。

sst.config.ts
// GET
api.route("GET /api/get",
  {
    handler: "src/get.handler",
    architecture: "arm64",
    link: [table], // tableをリンクすることで必要なポリシーが自動で付与される
    logging: {
      format: "json",
    },
  }
);

// POST
api.route("POST /api/post",
  {
    handler: "src/post.handler",
    architecture: "arm64",
    link: [table], // tableをリンクすることで必要なポリシーが自動で付与される
    logging: {
      format: "json",
    },
  },
);
src/get.ts
import { Resource } from "sst";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';

export async function handler() {
  const client = DynamoDBDocumentClient.from(new DynamoDBClient());
  
  const data = await client.send(new GetCommand({
    TableName: Resource.MyTable.name, // DynamoDBテーブルを参照
    Key: { code: "1" },
  }));

  return {
    statusCode: 200,
      body: JSON.stringify(data.Item),
  };
}
src/post.ts
import { Resource } from "sst";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';

export async function handler() {
  const client = DynamoDBDocumentClient.from(new DynamoDBClient());

  await client.send(new PutCommand({
    TableName: Resource.MyTable.name, // DynamoDBテーブルを参照
    Item: {
      code: "1",
    },
  }));

  return {
    statusCode: 200,
    body: "POST success",
  };
}

Linksを使うことで以下のメリットがあります。

  • AWSリソースをランタイムで参照したい時、.envなどの環境変数経由で値を渡す必要がなくなるので、環境変数で管理すべき対象が減る
  • CDKやTerraformなどのIaCでは、Lambda関数やS3などのAWSリソースに付与するポリシーを設定するためにそれぞれ専用の記述が必要だったが、そんなことを考える必要がなく、記述も一行で済むため非常に楽である

5. デプロイする

フロントエンド環境構築のデプロイと同じコマンドです。
これでAPI Gateway、Lambda、DynamoDBリソースが作成されます。

AWS_PROFILE=my-account npx sst deploy --stage dev

これで、ひとまずdev環境の構築が完了となります。

開発環境を起動

こちらのドキュメントを参考にします。

開発環境は、以下コマンドで起動できます。

AWS_PROFILE=my-account npx sst dev

これにより、指定したAWSアカウントに対して、アプリが開発モードで個人ステージにデプロイされます。

また、sst deployと違い、ローカル開発用に最適化されています。

  • Lambda関数をライブで実行します。これにより、すべてのリクエストがローカルマシンにプロキシされるため、ローカルマシン上のLambda関数の修正が即時反映される ようになります

  • Lambda関数実行時のログがターミナルで確認できるようになります(console.logなど)

  • フロントエンドやコンテナサービスはデプロイされません。代わりに、ローカルでdevサーバーが起動します

  • また、VPC にデプロイされているすべてのリソースに接続できるトンネルも作成されます

Lambda関数用ターミナル

スクリーンショット 2025-01-08 14.21.07.png

フロントエンドdev server用ターミナル

スクリーンショット 2025-01-08 14.21.18.png

Lambda関数

以下は、sst devにより生成されたLambda関数です。

スクリーンショット 2025-01-08 14.22.56.png

赤枠部分は個人名になるのと、説明欄には(live)という記述が追加されます。

API Gateway

個人用のAPI Gatewayが作成されています。

スクリーンショット 2025-01-08 18.13.37.png

DynamoDB

個人用のDBが作成されています。

スクリーンショット 2025-01-08 18.13.10.png

Secretsの扱いについて

SST では、.env による管理は推奨されていません。
先ほど説明したLinksと、これから説明するSecretsの利用を推奨しています。

環境毎に.envファイルが存在してしまう事や、それらをチームメイトと共有する事に対する指摘のようです。

sst secret コマンドを利用します。

一つずつ作成

AWS_PROFILE={{profile名}} npx sst secret set <name> [value] // local環境
AWS_PROFILE={{profile名}} npx sst secret set <name> [value] --stage dev // dev環境

ファイル読み込みして作成

AWS_PROFILE={{profile名}} npx sst secret load load ./secrets.env // local環境
AWS_PROFILE={{profile名}} npx sst secret load load ./secrets.env --stage dev // dev環境

ファイル内

secrets.env
KEY_1=VALUE1
KEY_2=VALUE2

内容確認

AWS_PROFILE={{profile名}} npx sst secret list // local環境
AWS_PROFILE={{profile名}} npx sst secret list --stage dev // dev環境

削除

AWS_PROFILE={{profile名}} npx sst secret remove <name> // local環境
AWS_PROFILE={{profile名}} npx sst secret remove <name> --stage dev // dev環境

Secretsの保存先

S3バケットが自動で生成され、その中に環境毎のSecretsが保存されます。
赤枠はsst devにより生成された私の名前です。
dev.jsonは、AWS_PROFILE=my-account sst secret set [value] --stage dev により生成されたSecretsです。

スクリーンショット 2025-01-08 14.43.42.png

Secretsの利用方法

例えばDomainというキーをSecretsに登録しておくと、以下のようにして参照できます。

sst.config.ts
const domainSecret = new sst.Secret("Domain");

new sst.aws.StaticSite("MyWeb", {
  domain: {
    name: domainSecret.value
  },
})

api.route("POST /api/post",
  {
    handler: "src/post.handler",
    architecture: "arm64",
    link: [table, domainSecret], // これでランタイムで使用できるようになる
    logging: {
      format: "json",
    },
  },
);

CI/CD

GithubActionsか、auto deployというSSTで提供されている機能を使うかの2択のようです。

auto deploy機能ですが、Beta版の間は無料との事です。

裏を返せばいつか有料になるという事だと思うので、今回はGitHub Actions で sst deploy を実行する形とします。

事前準備

Assume Role するための IAM ロールの作成を行います。

また、GitHub Actions の環境作成(prod, stage, dev)・シークレット(AWS_ACCOUNT_ID, ASSUME_ROLE_NAME) の登録も行っておきます。

ブランチの運用は main, stage, dev とします。

SSOログインまわりについて詳しくは以下参考ください:

構成ファイル

以下に、PRがマージされたタイミングで環境毎にdeployが走るシンプル構成のyamlを記載します。

sstDeploy.yaml
name: sst deploy

on:
  pull_request:
    branches:
      - main
      - stage
      - dev
    types:
      - closed

permissions:
  id-token: write
  contents: read

jobs:
  set-stage-name:
    runs-on: ubuntu-latest
    outputs:
      stage_name: ${{ steps.set-stage.outputs.stage_name }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set stage name
        id: set-stage
        run: |
          if [ "${GITHUB_REF#refs/heads/}" = "main" ]; then
            echo "stage_name=prod" >> $GITHUB_OUTPUT
          elif [ "${GITHUB_REF#refs/heads/}" = "stage" ]; then
            echo "stage_name=stage" >> $GITHUB_OUTPUT
          else
            echo "stage_name=dev" >> $GITHUB_OUTPUT
          fi

  deploy:
    needs: set-stage-name
    runs-on: ubuntu-latest
    environment: ${{ needs.set-stage-name.outputs.stage_name }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: "arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.ASSUME_ROLE_NAME }}"
          aws-region: ap-northeast-1

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install npm packages
        run: npm install

      - name: SST Deploy
        run: npx sst deploy --stage ${{ needs.set-stage-name.outputs.stage_name }}

さいごに

カスタマイズ編として、CloudFrontにおけるedge function設定(Basic認証)や、オリジン・ビヘイビア追加、API GatewayのLambdaAuthorizer設定追加など書こうとしたのですが、ボリューム的に別記事にしようと思います。

以前はCDK、SAM、LocalStackを使った記事を書き、この時も嬉しい気持ちになりましたが、今回さらにSSTの方がメリット多いと感じます。

SSTでは開発環境におけるLambda関数の修正が即時反映されるというのは知っていましたが、IaCとしての設定ファイルの記述の簡潔さが想像以上ですし、LinksやSecretsの仕組みにより管理すべき対象もシンプルになったと感じています。

あと、ターミナルでdeploy時に発生したエラー内容もCDKより具体的ですし、レスポンスが返ってくるスピードも速いので、IaC記述・構築もスムーズに行きました。

早速アプリに組み込んで動かしてみましたが、従来よりプロジェクト内のディレクトリ構造がシンプルになり、IaC設定ファイルもsst.config.ts一つになったので、これから新しく入ってくるメンバーのキャッチアップが従来より楽になると思います。
それではまた〜。

19
12
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
19
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?