AWS Lambdaからコンテナ(AWS Fargate)を色々使って呼び出してみた。


はじめに

Web APIを提供する際に、Amazon API GatewayとAWS Lambdaを組み合わせて実装するケースがあるかと思います。API Gateway + Lambda 関数で処理を完結するケースもあれば、既に実装されている他のサービスを呼び出して処理を完結させるケースもあるでしょう。今回は、後者、つまり、ALambda関数が他のサービスを呼び出して処理を完結するケース、具体的には、AWS Lambdaが AWS Fargateのタスクで実装されたサービスを呼び出す場合の方法と応答結果について書きたいと思います。

ちなみに、2020年8月2日に東京リージョンのAWS Lambdaを使って試した結果であり、仕様について、挙動について解説するものではありません。

要約

  • Lambda関数から NLB/ALBを経由で呼び出す場合とPriateHostedzoneに登録されたサービス名で呼び出す場合のレイテンシーは大差ない結果となった
  • CloudMap経由で呼び出す場合、それだけでCloudMapの応答時間が上乗せされてレイテンシーが大きくなる結果となった。

1.想定ケース

まず、想定ケースについて説明する前に、1点お伝えすることとして、冒頭ではAPI Gatewayのバックエンドという形で例に挙げましたが、Lambda関数のイベントソースは、この解説では重要ではありません。Amazon API Gatewayだろうと、Amazon SQSだろうと、AWS Step Functionsだろうと何でもよいです。今回の記事のポイントはAWS LambdaがAWS Fargateのタスクを呼び出す部分がポイントです。

今回の記事では、Lambda関数からAWS Fargateのタスク、つまり、コンテナをLambda関数からHTTPで呼び出す前提とます。

AWS FargateはVPCで動くAWSサービスです。VPCで動くということは、つまり、VPC のいずれかの Subnet の中に配置され、Private IP アドレス(設定によってはPublicIPアドレスも)をAWS Faragetのタスク単位に保持する形になります。今回は以下の想定でLambda関数から AWS Fargateのタスクにアクセスすることとします。

  1. Lambda 関数からはFargateに対して今回は、HTTPで通信を行い、アクセスすることを想定します。
  2. Fargateのタスクは、インターネットから直接アクセスができない、いわゆる、プライベートなSubnetに今回は配置されている前提 とします。
  3. 上記前提により、プライベートなSubnetに配置されたFargateのタスクにAWS Lambdaがアクセスするために、Lambda関数は、VPC内リソースにアクセスするためのLambda関数の設定を有効化にします。

なお、Fargateを構成された方はFargateを直接Lambdaから呼び出すケースなの?と疑問に思われるかもしれませんが、それについては、2.2で説明します。

2 Lambda関数からAWS FaragetのタスクにHTTPでアクセスするために

2.1 サービスディスカバリ

AWS Fargateのタスクに対してHTTP経由でアクセスする方法としては、幾つかあります。
AWS Fargateのタスク、つまり、コンテナはスケールイン・スケールアウトの設定次第ではありますが、随時、起動・停止が行われたり、不具合が発生すると新規のタスクが起動されます。つまり、IPアドレスが都度かわっていくため、HTTPでアクセスするには、その時動いているタスクのIPを正しくみつけ、アクセスする必要があります。では、刻々と変わっていくIPアドレスを取得するには、何が必要かというと、IPアドレスを登録する「レジストリ」とそのレジストリにアクセスしてIPアドレスを取得する仕組みが必要となります。

このように動的に変わっていくサービスの実行インスタンスを発見することをサービスディスカバリといいます。サービスディスカバリをするためには、レジストリとそのレジストリにアクセスして位置情報(今回はIP)を取得するアプリが必要です。

2.2 IPアドレスの登録(レジストリ)

AWS Fargate のタスクが起動するとそのタスクのIPアドレスの登録先として以下があります。
1. ALBのターゲットグループ
2. NLBのターゲットグループ
3. AWS CloudMap
4. Route53 のPrivate Hostedzone

上記は、AWS Fargateのサービス構成時に設定が可能で、3,4のケースではサービス名を独自に決定して登録することで、そのサービス名で名前解決、つまり、IPアドレス(設定方法によっては、ポート番号も)を取得することができます(つまり、Lambda内から、独自に定義した名前でアクセスすることが可能)

なお、「1. 想定ケース」で説明した通り、今回のLambda関数は、プライベートなSubnetでアクセスするするFargateにアクセスしますが、前述の2.1.1で登場した方法としてALBやNLBを利用してアクセスする場合は、ALBやNLBがどこに配置されているかによって、経路が変わります。今回は、Fargateが配置されたSubnetと同じSubnetにALB/NLBを配置する前提とします。

2.3 AWS LambdaがFargateにアクセスする場合のサービスディスカバリ

まず、ALB/NLB経由でアクセスする場合には、NLB/ALBの名前解決ができればALB/NLBがFargateに接続をするため、Fargateのタスクのサービスディスカバリについて気にする必要はありません。

また、Route53のPrivate Hostedzoneを利用する場合も、「名前解決」という意味では、ALB/NLBのケースと同じでDNSによる名前解決でサービスのIPアドレスを取得できるため、特別なことはありません。

一方、AWS CloudMapを利用する場合はアプリケーション、つまり、ここではLambda関数側でCloudMapのAPIでサービスの情報を取得し、そこからサービスにアクセスする形となります。

3.動作確認

ということで、今回は上記の4つのパターンについて実際にアクセスをしその結果を計測したいと思います。
いずれのパターンも名前解決が必要(具体的には、ALBやNLB,CloudMapの名前解決が必要)で、HTTPでアクセスるとリゾルバが Amazon DNS サーバーに問い合わせて解決をしアクセスします。CloudMapの場合は、CloudMapのエンドポイントの名前解決は Amazon DNS サーバーで実施し、アクセスしたいサービス(Farageのタスクとして動いているサービス)の解決をCloudMapが行います。また、Route 53のPrivate Hostedzoneを利用するケースは2.3で書いた通りALB/NLBと同じでHTTPアクセスすれば自動的に名前解決がされて考慮が不要となります。。

No. 登録先レジストリ IP登録方法 名前解決 経路
1 ALBターゲットグループ ECSタスク起動時にIPを登録 ALBの名前解決をDNSで実施 Lambda関数->ALB->Fargate
2 NLBターゲットグループ ECSタスク起動時にIPを登録 NLBの名前解決をDNSで実施 Lambda関数->NLB->Fargate
3 CloudMap ECSタスク起動時にIPを登録 CloudMapで名前解決 Lambda関数->CloudMap(IPを応答)->Lambda関数->Fargate
4 Route53 Private Hostedzone ECSタスク起動時にCloudMap経由でIPを登録 Route53 で名前解決 Lambda関数->Fargate

さて、1点重要なこととして、PrivateなSubnetにAWS Lambdaをもし配置した場合、注意する点として、NAT Gateway等を経由してCloudMapにアクセスする必要があるという点です。今回はケース3については、NAT Gateway経由でCloudMap APIを呼び出している。

app.py
import boto3
import os
import requests
from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.core import patch_all
patch_all()
servicediscovery = boto3.client('servicediscovery')
appId=os.environ['AppId']
envId=os.environ['EnvId']
serviceName=os.environ['ServiceName']
def lambda_handler(event, context):
  for num in range(2):
    httpRequestByRoute53()
    httpRequestByCloudMap()
    httpRequestByALB()
    httpRequestByNLB()
  return
def httpRequestByALB():
  response= requests.get("http://internal-FargatXXXXX.ap-northeast-1.elb.amazonaws.com/")

def httpRequestByNLB():
  response= requests.get("http://InternalNLB-8f6bXXXXXXXX.elb.ap-northeast-1.amazonaws.com/")
def httpRequestByCloudMap():
  instances = servicediscovery.discover_instances(
        NamespaceName='fargatetest',
        ServiceName='hogehoge2',
    )
  ipaddress= instances['Instances'][0]['Attributes']['AWS_INSTANCE_IPV4']
  response= requests.get("http://"+ipaddress+"/")

def httpRequestByRoute53():
  response= requests.get("http://hogehoge2.fargatetest/")

3.処理結果

各ケースについて、以下、X-Rayによる測定結果は以下の通り。なお、上記ソースコードにあるとおり、Lambda関数からサービスを呼ぶ際にはループで二回呼び出している。

No. アクセス先 関数処理時間 呼び出し先処理時間
1 ALB経由 90ms 40ms
2 NLB経由 82ms 37ms
3 CloudMap利用・直接アクセス 205ms 53ms
4 Route53 Private Hostedzone利用、直接アクセス 106ms 46ms

ALB/NLB間は僅かな差であり、何回か試したが時にはALBのほうが速い場合もあったため、誤差の範囲といえる。また、Route 53経由の場合も今回計測した結果からは誤差の範囲内といえる。
ただ、CloudMap経由の場合は、CloudMapを呼び出すのに数十msかかっており、これがオーバーヘッドとなって遅延が発生する。
実際のX-Rayの画面は以下の通り。

3.1 ALB経由

ALB経由でFargateにアクセス

3.2 NLB経由

NLB経由でFargateにアクセス

3.3 CloudMap経由

Lambda関数から二つのサービスを呼び出しており、servicediscoveryとなっているのが、CloudMapのAPIである。そして、CloudMapの戻り値を利用して10.0.10.85(Fargate)にアクセスしている。

3.4 Route53 Private Hostedzone 利用時

ECSのサービスで定義した名前(hogehoge2.fargatetest)で直接アクセスしている。

4.サマリ

CloudMapを利用してアクセスすると他の方法に比べてAPI呼び出しのオーバーヘッドが発生するため処理時間としてはマイナスな結果となった。コンテナのIPアドレスは動的に変わっていくことを考えると、キャッシュをしておくのが良いか、接続してみて404が応答されたらCloudMapを問い合わせる形にするのか・・・・何か良い方法をご存じの方がいればお教えください。