はじめに
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コマンド実行時にこの認証情報が参照されます。
[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とします。
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
// 以下追記する
ssr: false,
nitro: {
preset: 'aws-lambda'
},
devtools: { enabled: true }
})
2. sst.config.tsの編集
ドキュメントによると、Nuxtコンポーネントを用いる形で記載されています。
これを用いると、CloudFrontの設定において、静的ファイル専用のビヘイビアだけでなくNuxtのAPIサーバー専用のビヘイビアも追加されます。
new sst.aws.Nuxt("MyWeb");
しかし、今回Nuxtアプリはフロントエンドの静的ファイルを生成するのみの用途としたいので、StaticSiteコンポーネントを用いる形とします。
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の許可設定だけしておきます。
const api = new sst.aws.ApiGatewayV2("MyApi", {
cors: {
allowOrigins: [
// cors許可するドメイン名
"ドメイン名"
]
}
});
2. Lambda
API GatewayのRouteに対してLambda関数を紐づける部分はApiGatewayV2のドキュメントを見つつ、Lambda関数自体の細かい設定はFunctionのドキュメントを確認し、設定値を調整する形となります。
// 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のテーブルを作成します。
const table = new sst.aws.Dynamo("MyTable", {
// codeをパーティションキーとする
fields: {
code: "string"
},
primaryIndex: { hashKey: "code" }
});
4. Links
Lambda関数は、何も設定していないとDynamoDBへアクセスする事ができません。
Lambda関数自体のポリシーの設定や、ランタイムでのDynamoDBのテーブルへの参照が必要になります。
SSTにはlinkという便利なSDKが用意されています。
これを使うことでポリシーの設定や、ランタイムでのAWSリソースへの参照が容易になります。
// 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",
},
},
);
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),
};
}
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関数用ターミナル
フロントエンドdev server用ターミナル
Lambda関数
以下は、sst devにより生成されたLambda関数です。
赤枠部分は個人名になるのと、説明欄には(live)という記述が追加されます。
API Gateway
個人用のAPI Gatewayが作成されています。
DynamoDB
個人用のDBが作成されています。
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環境
ファイル内
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です。
Secretsの利用方法
例えばDomainというキーをSecretsに登録しておくと、以下のようにして参照できます。
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を記載します。
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一つになったので、これから新しく入ってくるメンバーのキャッチアップが従来より楽になると思います。
それではまた〜。