LambdaからALB経由でAPIを呼び出し、HTTPステータスを監視してみた

Lambdaのアイキャッチ

今回は、AWS LambdaからALBのDNS名を利用してAPIを呼び出す構成を作ってみました。

LambdaからALB配下のAPIを実行し、レスポンスコードを確認します。

また、HTTPステータスコードが300以上の場合はLambdaをエラー終了させることで、API監視に利用できる構成にしています。

最終的には、以下のような構成です。

EventBridge Scheduler
        │
        ▼
      Lambda
        │
        │ HTTPS
        ▼
       ALB
        │
        ▼
       EC2

今回の構成

今回は、EC2上に簡単なAPIを用意し、ALB経由でLambdaから呼び出します。

  • EC2:APIを提供
  • ALB:HTTPSリスナー(自己証明書を使用)
  • Lambda:APIを呼び出し
  • CloudWatch Logs:API呼び出し結果を記録
  • EventBridge Scheduler:Lambdaを定期実行

EC2にAPIを用意する

まず、EC2上にAPIとして利用するファイルを作成します。

sudo vi /var/www/html/http-trigger-test

以下の内容を記載します。

Hello from EC2 API!

ファイルが作成されていることを確認します。

sudo ls /var/www/html/http-trigger-test

ALBのDNS名を利用して、以下のURLでアクセスします。

https://<ALB_DNS_NAME>/http-trigger-test

正常にアクセスできると、以下のレスポンスが返却されます。

Hello from EC2 API!

SAN付きの自己署名証明書を作成する

今回は、LambdaからALBへHTTPS通信を行います。

そのため、ALBのHTTPSリスナーに自己署名証明書を設定します。

秘密鍵を作成する

まず、秘密鍵を作成します。

sudo openssl genrsa -out server.key 2048

OpenSSLの設定ファイルを作成する

次に、openssl.cnf を作成します。

vi openssl.cnf

以下の内容を記載します。

[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no

[req_distinguished_name]
C = JP
L = Default City
O = Default Company Ltd
CN = <ALB_DNS_NAME>

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = <ALB_DNS_NAME>

今回は、SAN(Subject Alternative Name)にALBのDNS名を指定しています。

SAN付き証明書を作成する

以下のコマンドで証明書を作成します。

sudo openssl req -x509 -new -key server.key -out server.crt -days 365 -config openssl.cnf

SANを確認する

作成した証明書にSANが設定されているか確認します。

openssl x509 -in server.crt -text -noout | grep -A 1 "Subject Alternative Name"

以下のように、ALBのDNS名が表示されればOKです。

X509v3 Subject Alternative Name:
    DNS:<ALB_DNS_NAME>
DNS名確認

証明書をALBのHTTPSリスナーに紐付ける

作成した自己署名証明書をAWSへインポートします。

その後、ALBのHTTPSリスナーに証明書を紐付けます。
紐付けの方法は下記記事の「ALBのリスナー設定を変更する」を参考にしてください。

【AWS】ALBとEC2間をHTTPS化する① ~まずはクライアント⇔ALB間をHTTPS化してみた~


ALBに設定された証明書を確認する

証明書をALBのリスナーに紐付けた後、証明書のシリアル番号を比較して確認します。

ローカルにある証明書のシリアル番号を確認

openssl x509 -in server.crt -serial -noout

ALBから返却される証明書のシリアル番号を確認

openssl s_client \
  -connect <ALB_DNS_NAME>:443 \
  -servername <ALB_DNS_NAME> \
  2>/dev/null | openssl x509 -serial -noout
openssl s_client -connect <ALB_DNS_NAME> -servername alb-api-test-621678629.ap-northeast-1.elb.amazonaws.com 2>/dev/null | openssl x509 -serial -noout

2つのシリアル番号が一致すれば、ALBに想定した証明書が設定されています。


Lambdaを作成する

次に、Lambdaを作成します。

今回は、LambdaからHTTPSでALBのAPIを呼び出します。

Lambda用のフォルダは以下の構成にします。

Lambda用フォルダ
├── lambda_function.py
└── server.crt

今回、LambdaからHTTPS通信を行うため、自己署名証明書である server.crt をLambdaに含めます。
server.crt は本手順で作成した証明書をダウンロードして(またはコピペ)ください。

コードソース確認

Lambdaのコード

lambda_function.py に以下のコードを記載します。

import urllib.request
import urllib.error
import ssl
import os
import logging


logger = logging.getLogger()
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
    url = "https://<ALB_DNS_NAME>/http-trigger-test"

    cert_path = os.path.join(
        os.path.dirname(__file__),
        "server.crt"
    )

    ssl_context = ssl.create_default_context(
        cafile=cert_path
    )

    try:
        with urllib.request.urlopen(
            url,
            context=ssl_context
        ) as response:
            status_code = response.status
            body = response.read().decode("utf-8")

    except urllib.error.HTTPError as error:
        status_code = error.code
        body = error.read().decode("utf-8")

    print(f"Request URL: {url}")
    print(f"Response Status Code: {status_code}")

    if status_code >= 300:
        logger.error(
            f"API request failed. "
            f"URL={url}, "
            f"StatusCode={status_code}, "
            f"ResponseBody={body}"
        )

        raise RuntimeError(
            f"API request failed. "
            f"URL={url}, "
            f"StatusCode={status_code}, "
            f"ResponseBody={body}"
        )

    return {
        "statusCode": status_code,
        "body": body
    }

コードのポイント

今回のポイントは、HTTPステータスコードが300以上の場合にLambdaをエラー終了させていることです。

if status_code >= 300:

例えば、以下のような場合です。

HTTPステータスLambda
200成功
201成功
204成功
300エラー
404エラー
500エラー

API監視を目的としているため、300以上を異常として扱うようにしています。

また、以下のログをCloudWatch Logsへ出力します。

print(f"Request URL: {url}")
print(f"Response Status Code: {status_code}")

そのため、どのURLを呼び出したか、どのHTTPステータスが返却されたかを確認できます。


LambdaをZIP化する

Lambdaへアップロードするため、コードと証明書をZIP化します。

zip lambda_function.zip \
  lambda_function.py \
  server.crt

ここで重要なのは、lambda_function.py と server.crt がZIPの直下に配置されていることです。

lambda_function.zip
├── lambda_function.py
└── server.crt

AWS公式でも、LambdaのPythonコードと必要なファイルはZIPのルートに配置する形が案内されています。


Lambdaをテストする

Lambdaダッシュボードの**「テスト」タブ**を開きます。

「イベントJSON」に以下を入力します。

{}

その後、「テスト」をクリックします。

APIが正常に呼び出せると、以下のようなレスポンスになります。

{
  "statusCode": 200,
  "body": "Hello from EC2 API!\n"
}

CloudWatch Logsには、以下のようなログが出力されます。

Request URL: https://<ALB_DNS_NAME>/http-trigger-test
Response Status Code: 200

CloudFormationでEventBridge SchedulerとLambdaを作成する

今回は手動でLambdaを作成しました。

しかし、実際には定期的にLambdaを実行してAPIを監視したいため、EventBridge SchedulerからLambdaを呼び出す構成にします。

以下のCloudFormationを利用すると、以下のリソースをまとめて作成できます。

  • CloudWatch Logs
  • Lambda実行ロール
  • Lambda
  • EventBridge Scheduler実行ロール
  • EventBridge Scheduler

CloudFormationテンプレート

AWSTemplateFormatVersion: '2010-09-09'

Description: >
  CloudFormation template for EventBridge Scheduler and Lambda

Parameters:
  LambdaCodeBucket:
    Type: String
    Description: Lambda deployment package S3 bucket name

  LambdaCodeKey:
    Type: String
    Description: Lambda deployment package S3 key

  ScheduleExpression:
    Type: String
    Default: cron(0 0 * * ? *)
    Description: EventBridge Scheduler schedule expression

Resources:

  ##################################################
  # Shared CloudWatch Logs Log Group
  ##################################################
  SharedLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: "/aws/lambda/request-api-logs"
      RetentionInDays: 120

  ##################################################
  # Lambda Execution Role
  ##################################################
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${AWS::StackName}-lambda-execution-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole

      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  ##################################################
  # Lambda
  ##################################################
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub ${AWS::StackName}-lambda
      Runtime: python3.12
      Handler: lambda_function.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn

      Code:
        S3Bucket: !Ref LambdaCodeBucket
        S3Key: !Ref LambdaCodeKey

      Timeout: 60
      MemorySize: 128

      LoggingConfig:
        LogGroup: !Ref SharedLogGroup

  ##################################################
  # EventBridge Scheduler Execution Role
  ##################################################
  SchedulerExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${AWS::StackName}-scheduler-execution-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - scheduler.amazonaws.com
            Action:
              - sts:AssumeRole

      Policies:
        - PolicyName: InvokeLambda
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                Resource:
                  - !GetAtt LambdaFunction.Arn

  ##################################################
  # EventBridge Scheduler
  ##################################################
  EventBridgeScheduler:
    Type: AWS::Scheduler::Schedule
    Properties:
      Name: !Sub ${AWS::StackName}-scheduler

      FlexibleTimeWindow:
        Mode: 'OFF'

      ScheduleExpression: !Ref ScheduleExpression

      ScheduleExpressionTimezone: Asia/Tokyo

      State: ENABLED

      Target:
        Arn: !GetAtt LambdaFunction.Arn
        RoleArn: !GetAtt SchedulerExecutionRole.Arn

Outputs:

  LambdaFunctionArn:
    Description: Lambda function ARN
    Value: !GetAtt LambdaFunction.Arn

  SchedulerArn:
    Description: EventBridge Scheduler ARN
    Value: !GetAtt EventBridgeScheduler.Arn

  SchedulerExecutionRoleArn:
    Description: EventBridge Scheduler execution role ARN
    Value: !GetAtt SchedulerExecutionRole.Arn

  SharedLogGroupName:
    Description: Shared CloudWatch Logs log group name
    Value: !Ref SharedLogGroup

まとめ

今回は、LambdaからALB経由でHTTPSのAPIを呼び出し、HTTPステータスコードを確認する構成を作成しました。

ポイントは以下です。

  • LambdaからALBのDNS名へHTTPSアクセス
  • SAN付き自己署名証明書を利用
  • LambdaにCA証明書を配置
  • APIのレスポンスコードをCloudWatch Logsへ出力
  • HTTPステータスコードが300以上の場合はLambdaをエラー終了
  • EventBridge SchedulerでLambdaを定期実行
  • CloudFormationで関連リソースをIaC化

今回は1つのAPIを対象にしていますが、今後はAPIごとにLambdaを分け、共通のCloudWatch Logsへログを集約する構成も試してみたいと思います。