5xx台のStatusCodeを返すApplication Load BalancerのバックエンドをLambdaで作る

:::note info

この記事は[AWS Community Builders Advent Calendar 2023](https://qiita.com/advent-calendar/2023/aws-community-builders)、の13日目の記事をあとから埋めています。

:::

NTTテクノクロス株式会社の渡邉 洋平です。コミュニティ活動ではwatany名義で活動することも多いです。

AWS Community BuilderのCloud Operationsとして2022年から選出されておりますが、専門分野詐欺になっているのが最近の悩みです。

## 発端


旧TwitterことX(以下Twitter)でこういった書き込みがありました。


https://twitter.com/hatake_book/status/1736227412906774626

<details><summary>Twitterリンク参照できない場合の引用</summary><div>

> ALBのヘルスチェックには合格するけど、5xxエラーを返すWebサーバーを意図的に作る方法を考え中…
> 試しにCPUとメモリ負荷を上げてみたが、ALBのATWは異常検知しなかったな🤔


</div></details>

日本語話者が少なくなる一方の昨今、AWS Community Builderとして、このような身近な検証活動も残していく必要があると考えております。

ということで、この辺りの挙動の理解がはっきりしていなかったため、実際に手を動かして挙動を確認したいと思います。

## 検証1 ALBのバックエンドが5xxを返す構成

### 構成

今回は簡単のため、ALB＋Lambdaの構成をとりたいと思います。

環境はAWS CDKでこのように表現されます。

```ts
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as elbv2Targets from "aws-cdk-lib/aws-elasticloadbalancingv2-targets";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";

import { Construct } from "constructs";

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

    const vpc: ec2.Vpc = new ec2.Vpc(this, "VPC", {
      maxAzs: 2,
    });

    const lb = new elbv2.ApplicationLoadBalancer(this, "LB", {
      vpc,
      internetFacing: true,
    });

    const fn = new NodejsFunction(this, "HelloWorldFunction", {
      entry: "lib/lambda/index500.ts",
      functionName: "err-500",
      runtime: lambda.Runtime.NODEJS_20_X,
      vpc,
    });

    const listener = lb.addListener("Listener", { port: 80 });
    listener.addTargets("Targets", {
      targets: [new elbv2Targets.LambdaTarget(fn)],
      healthCheck: {
        enabled: true,
      },
    });
  }
}
```

### Web Server

`/`の場合509を返し、`/health`の場合200を返すサーバーです。記述はTypescriptとWeb Frameworkの[Hono](https://hono.dev/getting-started/aws-lambda)を利用しています。


```ts: index500.ts
import { Hono } from "hono";
import { handle } from "hono/aws-lambda";

const app = new Hono();

app.get("/health", (c) => {
  return c.text("ok!");
});

app.all("/", (c) => {
  return c.text("Opps!", 509, {
    "X-Custom": "for-debug",
  });
});

export const handler = handle(app);
```

### 検証結果

意図した結果が返ることがわかります。

```bash
# /health
curl -i http://Error5-LB8A1-jzY5dv8AjNDj-201391188.ap-northeast-1.elb.amazonaws.com/health
HTTP/1.1 200 OK
Server: awselb/2.0
Date: Mon, 18 Dec 2023 16:02:22 GMT
Content-Type: text/plain;charset=UTF-8
Content-Length: 3
Connection: keep-alive

ok!

# /
curl -i http://Error5-LB8A1-jzY5dv8AjNDj-201391188.ap-northeast-1.elb.amazonaws.com/
HTTP/1.1 509 
Server: awselb/2.0
Date: Mon, 18 Dec 2023 16:02:25 GMT
Content-Type: text/plain; charset=UTF-8
Content-Length: 5
Connection: keep-alive
x-custom: for-debug
```


## 検証2 ALBのバックエンドが5xxを返す構成 + 「/」にヘルスチェック

### 構成

検証1の構成を一部変更します。

```diff_typescript
    const listener = lb.addListener("Listener", { port: 80 });
    listener.addTargets("Targets", {
      targets: [new elbv2Targets.LambdaTarget(fn)],
+      healthCheck: {
+        enabled: true,
+      },
    });
```

### 結果

curlによるリクエストは成功しませんでした。コンソールを確認するとHealthCheckが失敗していたことが原因です。

![failed_healthcheck.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528454/84454728-fd7f-5533-2f97-00ea1de462c0.png)

ちなみにHealthCheckのステータスコードは変更できますが、下記ドキュメントを見る限り500台は未対応であると明記されているため、ALB HealthCheckでの実現は難しいでしょう。

> If the protocol version is HTTP/1.1 or HTTP/2, the possible values are from 200 to 499. You can specify multiple values (for example, "200,202") or a range of values (for example, "200-299"). The default value is 200.

https://docs.aws.amazon.com/elasticloadbalancing/latest/application/target-group-health-checks.html#target-health-states

## 検証3 ALBのバックエンドが5xxを返す構成 + 「/health」にヘルスチェック

### 構成

検証2の構成を一部変更し、Pathを追加します。

```diff_typescript
    const listener = lb.addListener("Listener", { port: 80 });
    listener.addTargets("Targets", {
      targets: [new elbv2Targets.LambdaTarget(fn)],
      healthCheck: {
        enabled: true,
+        path: "/health"
      },
    });
```

### 結果

これは成功しました！ 特に仕様上も懸念はなさそうなのでこれで良さそうです。

![ok_healthcheck.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528454/6489db14-c465-4d1c-80aa-37ff73a5848c.png)

## まとめ

- ALBで5xx台のStatusCodeを返す場合、HealthCheckのエンドポイントを分けて運用する。
- ALB - Lambda構成では、HealthCheckなしでも利用できるため、5xxを返すという目的自体は達成できる。
    - この状態のLambdaの疎通を解析する際は、この辺りを参考にする。
    https://repost.aws/ja/knowledge-center/lambda-troubleshoot-targets-for-albs

久々にALBを触ったのですが、CDKを使うことで効率よく進めることができました。それでは！