コストをちょっと削減したプロダクトの形

## 前書き
仲間と一緒に、カツカツで起業活動を行っています。
AWSを使用しているため、最近の円安により頭を悩ませていました。
どうにかランニングコストを下げるため、行った一部の施策を記録します。
![img-mv01-19.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/27ba3368-c381-8a09-bb1d-4dfb0bc7be24.jpeg)

使用する技術は以下の通りです。。
| 言語 | Typescript |  || |
|:-----------|:------------:|:------------:|:------------:|:------------:|
| フレームワーク       | tRPC        | Express         | Turborepo| Prisma|
| インフラ・ミドルウェア     | Docker      | AWS       | CDK | PostgreSQL |

## 既存構成
リポジト管理は`モノレポ`形式を採用してます、モノレポの簡単な説明は下記に載せます。

> モノレポとは、アプリケーションやマイクロサービスの全コードを単一のリポジトリに保存するパターン。

共用ライブラリは`packages`フォルダに集約し、各アプリケーションは`apps`フォルダに配置しています。
例えば、ログ機能は`packages`フォルダに置き、各アプリケーションから利用できるようにしています。デプロイ時には、Turborepo の機能で効率よくコンテナ化され、最適な状態で展開されます。

![7801E701-6544-4C03-90CF-EC0D3FAD7846_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/e617b9f1-1644-0523-009e-6e95cb8739af.jpeg)


インフラ構成は、以下の図に示すように、ベーシックな構成になっています。
![C22FF4EE-C471-4B09-9BFB-660D6953CD2A.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/ad03d482-5445-7af3-babf-eab9f21fc212.jpeg)

サービスのアクセス数がまだ少ないため、サーバーレス化を決定しました。

ただし、サーバーレス化はあくまで暫定的な対処です。今後、ユーザー数が増加した場合には、コストを抑えるためにECSへ戻すことも考慮しています。

## サーバーレス化

#### domain層のパッケージ化

正直、「domain層」という命名が正しいのか不安はありますが、
フロントエンドからパラメーターを受け取り、最初に処理を行う層として意識しています。

DB処理(infra層)のコードは最初からパッケージ化されているため、controller層が変更されてもアプリ全体への影響は少なく済みます。

![879CFCD1-77FE-4D30-9E96-DCC1F2E65DB9_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/36486bfd-65a9-e880-f1d2-038c570ca61d.jpeg)

この構造にすれば、今後移行作業が発生しても、人力コストをある程度削減できるはずです。

#### ALB + ECSからAPIGatway + Lambdaへ変更

変更前は`Express`を採用してましたが、よく調べてみたら、`tRPC`は`AWS APIGatway`をサポートしてるらしく、公式ドキュメント通りに`tRPC`使って置き換えました。

https://trpc.io/docs/server/adapters/aws-lambda

ただし、通常の.zipファイルを使用した`Lambda`デプロイではサイズ制限に引っかかってしまったため、Express採用時のDockerfileを改造して運用しました。
.zipの場合、解凍後サイズ250MBが上限ですが、イメージの場合は10GBです。

使用するDockerfileの一部抜粋を以下に載せておきます。

```yaml
Turborepo流儀の通常ステージビルド
...

FROM public.ecr.aws/lambda/nodejs:20
COPY --from=installer --chown=app:nodejs /app/pruned .
COPY --from=builder --chown=app:nodejs /app/apps/serverless-api/dist .

ENV NODE_ENV=production
CMD [ "index.handler" ]
```
サーバーレス化されたイメージのECRへのプッシュと別に、LambdaのデプロイにはCDKを採用しています。
使用したcdkのコードの抜粋も載せておきます。

```typescript:cdk.ts
import {
  aws_iam as iam,
  aws_lambda as lambda,
  aws_apigateway as apigateway,
  Stack,
  StackProps,
} from "aws-cdk-lib";
import { Handler, Runtime } from 'aws-cdk-lib/aws-lambda';
import { Construct } from "constructs";
import * as path from "path";

export class FormationStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    ... ecr、vpc、rdsなど

    const tRpcLambda = new lambda.Function(this, "TRpcLambda", {
      runtime: Runtime.FROM_IMAGE,
      handler: Handler.FROM_IMAGE,
      code: lambda.Code.fromEcrImage(ecrRepo, {
        tagOrDigest: 'latest',
      }), 
      vpc,
      role: lambdaRole,
      environment: {
        NODE_ENV: "production",
        DATABASE_URL: "",
      },
    });

    dbSecret.grantRead(tRpcLambda);

    const api = new apigateway.RestApi(this, "TRpcApi", {
      restApiName: "TRpcApi",
      description: "TRpcApi",
      endpointTypes: [apigateway.EndpointType.EDGE],
      deployOptions: { stageName: "v1" },
    });

    const apiResource = api.root.addResource("api");
    const anyResource = apiResource.addResource("{path+}");
    anyResource.addMethod("any", new apigateway.LambdaIntegration(tRpcLambda))

  }
}
```

## コスト削減の効果
![48565060-2A7F-44A1-8E4C-C0E2F804B822.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/118aff2d-2242-c905-05a1-87400fe71132.jpeg)

単純計算すると2.93 + 5.14 = 8.07%となります。
しかし、無料利用枠や従量課金制があるとはいえ、API Gateway と Lambda の構成も無料ではありません。
そのため、実際のコスト削減率はさらに少ないかもしれません:joy:

今後もコストに気を配りながら開発していきたいです。

## 終わりに
今は若干円高に戻りましたが、2024年7月10日前後には約161円になり、
サーバー代だけで倒産しそうになって冷や汗をかきました:scream:

コスト削減したことは無駄にはならないと思います、
コストを低く抑えられれば、サービスも長続きし、成功する可能性も上がると思います:relaxed:

欲を言えば、インフラを`cloudflare`利用すればもう少し安価にできたのかもしれません、今度試してみたいです。