Hono使ってLambdaにWebAPIを爆速公開

## はじめに

KG-7thの槙島です。

以前作成したTypeScriptベースのWebAPIをリメイクして手早く公開したいです。
調べてみてHonoフレームワークが一番適していると感じたため、試してみました。

## Honoのセットアップ

たま～に聞くHonoフレームワーク。
どうやら爆速で開発できるみたいなのでやってみよう。

https://hono.dev/docs/getting-started/aws-lambda

### 注意
デプロイでDockerが必要になるので、自分はVirtulBoxのUbuntuで実施してます。
aws configure ssoで認証情報を設定済みです。多分これしないとデプロイできないです。


### AWS CDK CLIをインストール


Honoライブラリの手順の中で出てきた。便利そうなのでこの機会に入れてみる

```bash
# インストール
npm install -g aws-cdk
# CDKのAWS環境内のセットアップ
cdk bootstrap
```

### lambda関数のテンプレを生成

```bash
cdk init app -l typescript
```

### Honoのインストール

```bash
npm i hono
```

### Lambda実行ファイルの作成

```bash
mkdir lambda
```
lambdaフォルダの中にindex.tsを作成

### index.tsの中身
```ts
import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))

export const handler = handle(app)
```

### lib/ans-automate-stack.tsの中身

```ts
import * as path from 'path';
import * as cdk from 'aws-cdk-lib'
import { Construct } from 'constructs'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as apigw from 'aws-cdk-lib/aws-apigateway'
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'

export class SnsAutomateStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    // Lambda Layerの作成 (assets を指定)
    const layer = new lambda.LayerVersion(this, 'arn:aws:lambda:ap-northeast-1:123456789012:layer:SnsAutomateLayer:2', {
      code: lambda.Code.fromAsset(path.join(__dirname, '../assets')),
      compatibleRuntimes: [lambda.Runtime.NODEJS_20_X], // Node.jsランタイムを指定
      description: 'Lambda Layer for SNS Automate project',
    });

    const fn = new NodejsFunction(this, 'lambda', {
      functionName: "SnsAutomateFunction",
      entry: 'src/index.ts',
      handler: 'handler',
      runtime: lambda.Runtime.NODEJS_20_X,
      layers: [layer], // 作成したレイヤーを追加
      timeout: cdk.Duration.seconds(20), // 少し長め
      memorySize: 256, // 128だと動かなかった
      role: cdk.aws_iam.Role.fromRoleArn(this, 'SnsAutomateFunctionRole', 'arn:aws:iam::123456789012:role/SnsAutomateFunctionRole'),
      environment: {
        ROOT_URL: '/opt', // レイヤー内のアセットを参照するために設定
      },
    })
    fn.addFunctionUrl({
      authType: lambda.FunctionUrlAuthType.NONE,
    })
    new apigw.LambdaRestApi(this, 'myapi', {
      handler: fn,
    })

    // CloudWatch Eventsルールを作成
    new cdk.aws_events.Rule(this, 'SnsAutomateWeatherTweetScheduleRule', {
      ruleName: "SnsAutomateWeatherTweetScheduleRule",
      // 毎朝7時に実行
      schedule: cdk.aws_events.Schedule.cron({ minute: '0', hour: '22' }),
      targets: [new cdk.aws_events_targets.LambdaFunction(fn)],
    });
  }
}


```


### Lambda Layerについて

画像やフォントなどの静的ファイルはそのままデプロイしてもついてきません。
Layerに入れることでLambda関数から扱うことができます。

今回はすでに作成済みのため、作成したLayerのARNを指定しています
（デプロイすることでLayerに静的ファイルがアップロードされるみたい）
```typescript
const layer = new lambda.LayerVersion(this, 'arn:aws:lambda:ap-northeast-1:123456789012:layer:SnsAutomateLayer:2', {
```

なお、まだ作成していない場合は適当な名前を入れるとLayerが作成されます
```typescript
    const layer = new lambda.LayerVersion(this, 'SnsAutomateLayer', {
```

### 関数にアタッチするロールについて

SnsAutomateFunctionRoleには以下のポリシーを許可しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4042336/d3041978-11b2-4200-a2cd-431891bd6c10.png)

SSMでパラメタを取得してきているのでSSMのread用のパーミッションを、
Cloudwatchのログを確認したいのでBasicExecute・・・を適用しています。

関数に紐づけているのは以下の箇所です。

```typescript
role: cdk.aws_iam.Role.fromRoleArn(this, 'SnsAutomateFunctionRole', 'arn:aws:iam::123456789012:role/SnsAutomateFunctionRole'),
```

## デプロイ

```bash
cdk deploy
```

デプロイ成功！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4042336/0cf282c9-5cf3-40bd-8978-f35be19a8cd6.png)

出てきたURLにアクセスして問題ないことを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4042336/1ebce8aa-1a5a-4565-b992-9fe0f923fd04.png)


## 結論

早い・・・
RemixのLambda-web-adapterで右往左往していた時の30倍は早いぞ・・・

今回のコードはこちら
これから使っていく予定なので、コミットのURLを載せておきます。。

https://github.com/undo-works/sns-automate/commit/d98e0245291041fb413892e9e10998eeaad969db

## 感想

CDKを使ったので、裏側で何が起きてるのか正直よくわかってない。
これからドキュメント見直してみます。

ところで、これ別の端末からデプロイしたら別のLambda関数としてデプロイされてしまうのでは。。


## 追記

気になったところをperplexityに聞いてみた

### 質問①

この場合、環境変数はどこで管理すればよいでしょうか？

### 回答


CDKで環境変数を管理する主な方法は3通りあります：

1. CDKスタック内での直接設定（非機密情報向け）
```typescript
const fn = new NodejsFunction(this, 'lambda', {
  entry: 'lambda/index.ts',
  environment: {
    API_KEY: 'your_api_key',
    STAGE: 'production'
  }
});
```
2. AWS Systems Manager Parameter Store（推奨）
```typescript
import * as ssm from 'aws-cdk-lib/aws-ssm';

const fn = new NodejsFunction(this, 'lambda', {
  entry: 'lambda/index.ts',
  environment: {
    DB_PASSWORD: ssm.StringParameter.valueForStringParameter(this, '/config/DB_PASSWORD'),
  }
});
```
3. AWS Secrets Manager（機密情報向け）
```typescript
import * as secrets from 'aws-cdk-lib/aws-secretsmanager';

const secret = secrets.Secret.fromSecretNameV2(this, 'Secret', 'my-secret');

const fn = new NodejsFunction(this, 'lambda', {
  entry: 'lambda/index.ts',
  environment: {
    SECRET_VALUE: secret.secretValue.toString(),
  }
});
```
### 質問②

デプロイ時のLambdaの設定
（なんという名前の関数としてデプロイするのか、どのVPCに接続するのかなど）を行うにはどうすればよいですか？

### 回答
CDKスタックの設定例：
```typescript
import * as ec2 from 'aws-cdk-lib/aws-ec2';

const fn = new NodejsFunction(this, 'lambda', {
  entry: 'lambda/index.ts',
  functionName: 'MyHonoFunction', // 関数名指定
  runtime: lambda.Runtime.NODEJS_20_X,
  memorySize: 512, // メモリサイズ
  timeout: cdk.Duration.seconds(10), // タイムアウト
  architecture: lambda.Architecture.ARM_64, // ARMアーキテクチャ
  
  // VPC設定
  vpc: ec2.Vpc.fromLookup(this, 'VPC', { vpcId: 'vpc-12345' }),
  vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
  securityGroups: [/* セキュリティグループ配列 */]
});
```

主要な設定項目：
* プロパティ	説明	デフォルト値
* functionName	Lambda関数名	CDK自動生成
* memorySize	メモリ割当量（MB）	128
* timeout	実行制限時間	3秒
* architecture	CPUアーキテクチャ	X86_64
* vpc	接続するVPC	なし
* securityGroups	適用するセキュリティグループ	自動生成
* ベストプラクティス：
* 関数名はfunctionNameで明示的に指定（CDKの自動生成名は可変）

VPC接続時はPrivate Subnet + NAT Gateway構成を推奨

開発環境と本番環境で設定を分ける場合：

```typescript
const isProduction = process.env.STAGE === 'production';

new NodejsFunction(this, 'lambda', {
  memorySize: isProduction ? 1024 : 256,
  timeout: isProduction ? 30 : 10
});
```

補足
記事のサンプルコードを拡張する場合は、CDKスタックファイル（lib/*-stack.ts）のLambda関数定義部分を修正します。デプロイ時はcdk deployを再実行すれば、変更内容が自動反映されます。


とのこと。まだ試してないので参考程度に。