AWS Lambda関数の呼び出し方を改めて確認する(2)

### はじめに
当記事は[AWS Lambda関数の呼び出し方を改めて確認する(1)](https://qiita.com/horit0123/items/0d82d999782d4e4540e5)の続きとしてLambda関数の呼び出し方のパターンを詳細に整理し確認していくものです。
　前述の記事では、WebアプリケーションのバックエンドとしてLambdaを実装した場合の例で呼び出し方としてご紹介しました。Lambda関数の基本的な呼び出し方（起動方法）としては、AWS CLI/AWS SDK/AWSサービスのとの連携という形でご説明しました。当記事では、Lambda関数がLambda関数を呼び出す場合について記載したいと思います。特にVPCと絡めて整理します。

### サマリ
- Lambda関数にはVPC内リソースにアクセスするLambda関数がある
- VPC内リソースにアクセスするLambda関数が別のLambda関数を呼び出す際(※1)にはPublic（インターネット経由）でLambdaのエンドポイントにアクセスする必要がある(※2)。
- VPC内のリソースにアクセスするAWS Lambda関数から接続する方法としては、
  - Lambda関数のENI(Elastic Network Interface)が作成されるSubnetにインターネットに接続可能なルートを設定する方法
  - プライベート用API Gatewayを構築し、API Gateway経由で該当のLambda関数を呼び出す方法
  - ALB(Application Load Balancer）経由で該当のLambda関数を呼び出す方法
 
（※1）今回は別のLambda関数を呼び出す際と書きましたが、AWS Lambdaだけでなく、Publicな場所にあるAWSの各サービスのエンドポイントへのアクセスや、外部サービス等を呼び出す場合も含まれます）
（※2）2019/12/14時点ではLambdaに対するVPCエンドポイントがないため。


### Lambda関数を起動する際の接続先エンドポイント
AWSのサービス、例えば、IAMもSTSもRDSもEC2も、サービスのAPIを呼び出すためのエンドポイントがあります。サービス別・リージョン別の接続先エンドポイントは[こちら](https://docs.aws.amazon.com/ja_jp/general/latest/gr/rande.html#lambda_region)をご確認ください。（このリンクは、Lambdaのエンドポイント一覧に飛びます。)
 前回の記事でも例に挙げたAWS CLIでLambda関数を起動する場合にも内部の動作としては、HTTPSでAWS Lambdaのエンドポイントに接続していました。以下のログをご確認いただけます。

```
$ aws lambda invoke --function-name QiitaSample --payload '{"key":"test"}'  result.txt  --debug 2019-12-13 01:19:43,553 - MainThread - awscli.clidriver - DEBUG - CLI version: aws-cli/1.16.246 Python/2.7.16 Linux/4.14.154-99.181.amzn1.x86_64 botocore/1.12.236
(以下、一部省略や修正はあります）
2019-12-13 01:19:43,681 - MainThread - botocore.auth - DEBUG - Calculating signature using v4 auth.
2019-12-13 01:19:43,681 - MainThread - botocore.auth - DEBUG - CanonicalRequest:
POST
/2015-03-31/functions/QiitaSample/invocations

host:lambda.us-east-1.amazonaws.com
x-amz-date:20191213T011943Z
x-amz-security-token:IQoJc74Yn0csBySGcLa9utRBGrXg5tyTgjr7sU+OjPCIFgdfaj91dfj28jdq8de7fgf27yh8u9w01he1uix,jo

host;x-amz-date;x-amz-security-token
b1874bbcb2dc77cefaac4790df53b71aa6e65488e0342a286202764f15ebf80b
2019-12-13 01:19:43,682 - MainThread - botocore.auth - DEBUG - StringToSign:
AWS4-HMAC-SHA256
20191213T011943Z
20191213/us-east-1/lambda/aws4_request
cd6dfadfasfadfdfwf4r4t557j5:y;3:2;:rfl12pk1fhrtyaaerahstduerjdrgrafas123g23rh4fasdfas7
2019-12-13 01:19:43,683 - MainThread - botocore.auth - DEBUG - Signature:
968d2669
2019-12-13 01:19:43,683 - MainThread - botocore.endpoint - DEBUG - Sending http request: <AWSPreparedRequest stream_output=True, method=POST, url=https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/QiitaSample/invocations, headers={'Content-Length': '14', 'Authorization': 'AWS4-HMAC-SHA256 Credential=ASIAAVBCFEFFDFDFD/20191213/us-east-1/lambda/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=sdasdasdfasda', 'X-Amz-Security-Token': W0+7pqDjMUldzB8Y6AHws+GPcYdQIhAN2Ou6Wkl18MRd(一部省略) oFWnBQh6HU1K92QhejnnHox0TlYkYtB9953UHQi5QudqxA73XfSCZHSyemOICcbc74Yn0csBySGcLa9utRBGrXg5tyTgjKeLx/p3mr7sU+OjPCIFgIqx0DWP1vTcmi8k7KR0K2OOnmjLI=', 'X-Amz-Date': '20191213T011943Z', 'User-Agent': 'aws-cli/1.16.246 Python/2.7.16 Linux/4.14.154-99.181.amzn1.x86_64 botocore/1.12.236'}>
2019-12-13 01:19:43,739 - MainThread - urllib3.connectionpool - DEBUG - https://lambda.us-east-1.amazonaws.com:443 "POST /2015-03-31/functions/QiitaSample/invocations HTTP/1.1" 200 181
2019-12-13 01:19:43,740 - MainThread - botocore.parsers - DEBUG - Response headers: {'x-amzn-RequestId': 'b6fb107d-deff-4271-8bc0-e436afae2093', 'Content-Length': '181', 'x-amzn-Remapped-Content-Length': '0', 'X-Amz-Executed-Version': '$LATEST', 'X-Amzn-Trace-Id': 'root=1-5df2e72f-d85e5928ecf88298a1e61aee;sampled=0', 'X-Amz-Function-Error': 'Unhandled', 'Connection': 'keep-alive', 'Date': 'Fri, 13 Dec 2019 01:19:43 GMT', 'Content-Type': 'application/json'}
2019-12-13 01:19:43,741 - MainThread - botocore.parsers - DEBUG - Response body:
<botocore.response.StreamingBody object at 0x7fd2fa4d2cd0>
2019-12-13 01:19:43,741 - MainThread - botocore.hooks - DEBUG - Event needs-retry.lambda.Invoke: calling handler <botocore.retryhandler.RetryHandler object at 0x7fd2fa4d2250>
2019-12-13 01:19:43,742 - MainThread - botocore.retryhandler - DEBUG - No retry needed.
2019-12-13 01:19:43,742 - MainThread - botocore.hooks - DEBUG - Event after-call.lambda.Invoke: calling handler <bound method StreamingOutputArgument.save_file of <awscli.customizations.streamingoutputarg.StreamingOutputArgument object at 0x7fd2fa958f50>>
2019-12-13 01:19:43,743 - MainThread - awscli.formatter - DEBUG - RequestId: b6fb107d-deff-4271-8bc0-e436afae2093
{
    "FunctionError": "Unhandled", 
    "ExecutedVersion": "$LATEST", 
    "StatusCode": 200
}
 
```

### LambdaだってRDSにアクセスしたいぞ！の件
Lambda関数が処理をする際に必要なデータがRDSやElastiCache、あるいは、EC2内に格納されている場合、そのLambda関数は[VPC 内のリソースにアクセスするように Lambda 関数を設定する](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-vpc.html)構成が必要となります。
　それって**アンチパターン**でしょ？とう方もいらっしゃるかと思います。確かに、以前は

- VPC内リソースへのアクセスのためのENI作成の時間
- RDSに接続するLambdaが増えることによるRDSのConnection枯渇・リソース枯渇

という理由でアンチパターンでした。ただ、2019年9月に発表され順次展開中（東京リージョンは2019年9末に展開済み）の**[Announcing improved VPC networking for AWS Lambda functions](https://aws.amazon.com/jp/blogs/compute/announcing-improved-vpc-networking-for-aws-lambda-functions/)**によって、VPC内リソースへのアクセスでネックとなっていた起動時の遅延が改善され、また2019年12月2日に発表された**[Introducing Amazon RDS Proxy (Preview)](https://aws.amazon.com/jp/about-aws/whats-new/2019/12/amazon-rds-proxy-available-in-preview/)** により、コネクションプーリング機能がPublic Preview ではありますが、提供されるようになったため、必ずしもアンチパターンではなくなりました。詳細の検討や検証はここではしませんが、VPC内にあるRDS等のデータソースにアクセスしたい場合、以前よりも利用しやすい環境になったといえるかと思います。

### VPC内リソースにアクセスするLambda関数の構成とネットワーク経路
ここでは、VPC内リソースにアクセスするLambdaを説明し、構成した場合に発生するネットワーク経路について触れていきたいと思います。
　以下の図では、2つのSubnetと1つのSecurity Groupを設定した際の例を以下の図で示したいと思います。
　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535037/ee820472-1f7f-b1da-712d-f357d546524d.png)

上記の通り設定をすると指定したSubnetにENIが作成され起動されたLambda関数は指定されたSubnetからアクセス可能なRDS等のリソースにアクセスして処理を実施ます。以下は、作成されたENIです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535037/9353edf4-364f-f810-86d7-5773a44cccdc.png)

さて、ここで重要なことは次の部分です。こちらは、Lambda関数のVPCの定義をした際に表示されているメッセージの抜粋です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535037/680e709a-3ede-43f6-83b9-eba44ab7fd55.png)

つまり、VPC内リソースにアクセス可能なLambda構成を実施すると、そのLambda関数（ここでは、QiitaSample)から、どこかのサービスに通信を行う際には以下の仕組みで動きます。

- ENIが作成されたSubnetのルートテーブルやNACLの設定従い通信を行う
- ENIには、Security Groupもアタッチされており、Outbound通信が許可されていないと、通信はできない。

私が担当した案件でも仕組みが原因で困られたお客様がいらっしゃいました。
さて、上記の仕組みでは何が困るのでしょうか？
### VPCの構成を振り返る
VPCには様々なコンポーネントがありますが、ここでは以下の3つを中心に振り返りたいと思います。

- Subnet
- NAT Gateway
- Internet Gateway(IGW)

#### Subnet 
　まず、Subnet。SubnetはVPCのIPアドレスレンジを分割する物で、EC2、ElastiCache、RDS、RedshiftはVPC内のいずれかのSubnetに配置することで利用が可能になります。Subnetの分類の仕方には、インターネットに直接アクセス可能なPublic Subnetと、インターネットに直接アクセスできないPrivate Subnetに分類する方法があります。

|種類|特徴|見分け方|
|---|---|---|
|Private |PrivateなSubnet内のリソース（EC2, RDS, ElastiCache, ELB等)はインターネットから（inbound)の直接通信を受けることができない、**かつ**、そのリソースから直接インターネットへアクセス(outbound)することができない |そのSubnetのルートテーブルのルートにIGWへのルートがなければPrivate。いくら名前をPrivateと書いてもIGWの有無で判定します。|
|Public |PublicなSubnet内のリソース（EC2, RDS, ElastiCache, ELB等)はインターネットから（inbound)の直接通信を受ける構成が可能、**かつ**、そのリソースから直接インターネットへアクセス(outbound)する構成も可能|そのSubnetのルートテーブルのルートにIGWへのルートがあればPublic|


ちなみに、よりセキュアにシステム構築をすることを目的に私は Privateファーストでリソース配置を常に考え、必要であればPublicを検討するようにしています。

#### NAT Gateway
 先ほどSubnetがPublicとPrivateの二種類があるという説明をしましたが、PrivateなSubnetにEC2やRDSを配置すると、当然、インターネットにはアクセスできません。ただ、EC2がパッチを取得したい場合や外部のサービスを呼び出したい場合、PrivateなSubnetでは直接アクセスできませんので、解決方法の１つとしてPublicなSubnetに配置したNAT Gatewayを利用して外部と通信する方法があります。NAT GatewayはInternet Gateway経由で外部と通信を行い、その結果をリクエスト元に戻します。PrivateなSubnetにあるリソースは、直接インターネットに出れないだけで、構成をすればPublicなSubnetにあるリソースを経由してアクセスが可能になります。（今回はNAT Gateway)
 
#### Internet Gateway
 VPCにアタッチするコンポーネントでインターネットとアクセスする必要がある場合、IGWをVPCにアタッチし、さらに、SubnetのルートテーブルでIGWへのルーティングを設定します。そうすることで、そのSubnetのリソースはIGW経由でインターネットにアクセスが可能となります。つまり　Public Subnetということになります。

#### Internet GatewayがアタッチされないVPC
私が担当したプロジェクトではVPCにIGWがついていない、つまり、インターネットアクセスが遮断されたVPCがいくつもありました。インターネットとのやりとりは一切発生しない、あるいは、オンプレの既存のProxy経由で行うといった制御をされてる場合にIGWをアタッチしないVPCが構成されることがよくあります。
　IGWがアタッチされていないVPC内のEC2等がインターネットにアクセスしたい場合はDirect Connect経由で一度オンプレに通信を流し、そこから既存のオンプレミスに設けられたインターネットへの出口を使ってアクセスできるようにするケースもあれば、別途、外部接続用のVPCを独立した形で整備し、そのVPCと自身のVPCをVPC　PeeringやTransit Gatewayで連携して通信をするというケースもあります。

つまり、VPC内リソースにアクセス可能なLambda関数として構成することで、**構成する前は容易に通信できていたエンドポイントにアクセスできなくなり困る可能性がある**ということになります。もしLambda関数がVPC外のリソースへ接続する必要がある場合は事前に経路があるかどうか確認してみてください。

#### VPC内リソースにアクセスするLambdaが別のLambda関数やAWSサービスを呼びたい場合
　もし、皆さんのLambda関数がRDSに対してデータを参照・更新したあと、別のLambda関数を呼び出して処理をしたい場合、どうしますか？例えばPythonで動くLambda関数の場合以下のようなコードを実装するかと思います

```
response = client.invoke(
    FunctionName='QiitaSample2',
    InvocationType='RequestResponse',
    LogType='None',
    Payload='fileb://tmp/input.json'
)
```
　
ではこのLambda関数がVPC内リソースにアクセスする関数として構成され、**かつ**そのVPCにはInternet Gatewayがアタッチされていなかったらどうなるでしょうか？
答えは「**宛先に到達できず、Timeoutする**」です。これが私が担当したお客様で困られていた内容となります。ではどうすれば解決するのでしょうか。

### VPC内リソースにアクセス可能なLambda関数から別のLambda関数をどう呼ぶか（Internet GatewayがないVPC）

今回はアクセス方法を二種類ご紹介します。
1. PrivateなAPI Gateway経由案
2. Iternal ALB経由案


#### PrivateなAPI Gateway経由案
2018年秋より、Amazon API Gatewayでは
- エッジ最適化API
- リージョナルAPI
- プライベートAPI
の三種類のAPIがサポートされるようになりました。三種類の詳細は[こちら](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html)をご参照ください。今日は、この中のプライベートAPIを利用して、Lambda関数からLambda関数を呼ぶ方法をご紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535037/0801d4c9-7ee3-a083-5048-3e376a79fc8d.png)
こちらがPrivateなAPI経由でLambda関数を呼び出すシーケンスです。

**<<事前構成>>**
**A:**まず、APIGatewayでAPIをPrivateとして構成しLambda関数を呼び出すように構成します。
**B:**PrivateなAPIにアクセスするために必要なVPCエンドポイントを作成します。
**C:**Lambda関数をVPC内リソースにアクセス可能な構成にし、ENIをVPC内に作成します。
**<<シーケンス>>**
**1.**ユーザーがLambdaのエンドポイントにアクセスしLambda関数を起動します。
**2.**起動されたLambda関数のプログラムがAPIを呼び出します。通信はVPC内に作成されたENI経由でVPCエンドポイントに転送されます。
**3.**Private APIが呼び出され、Lambda関数を呼び出します。

上記の構成をすることで、IGWの無いVPCからLambda関数を呼び出すことが可能になりました。

#### Internal ALB経由案
ALBは[ターゲットグループ](https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-target-groups.html)に登録されたEC2インスタンス等に受け付けたリクエストを転送し応答をリクエスターに返します。2019年12月14日現在、ALBのターゲットグループがサポートするターゲットは

- IP
- インスタンス
- Lambda関数

の三種類になります。[こちら](https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-target-groups.html#target-type)で確認できます。ここでLambda関数を指定することで、ALBにアクセスできれば、Lambda関数を呼び出せるようになるというわけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535037/37ba400c-7d11-1a95-e103-5404aa7703c8.png)
こちらがInternal ALB経由でLambda関数を呼び出すシーケンスです。
**<<事前構成>>**
**A:**まず、ALBをInteranalなALBとして構成し、Private Subnetに配置しておきます。（想定としてIGWがないVPCを想定しているためInternalなALBとしています。）
**B:**ALBのターゲットグループに該当のLambda関数を指定し、ALBからアクセスできるようにLambda関数のアクセス許可ポリシーも設定しておきます。
**<<シーケンス>>**
**1.**ユーザーがLambdaのエンドポイントにアクセスしLambda関数を起動します。
**2.**起動されたLambda関数のプログラムがALBを呼び出します。通信はVPC内に作成されたENI経由でALBに転送されます。
**3.**ALBが呼び出され、Lambda関数を呼び出します。

この方法でもVPC内からLambdaを起動することができるようになりました。

### 参考
 今回IGWのないVPC内のリソースにアクセスするLambda関数がPublicなエンドポイントしか対応していないAWS Lambda関数を呼び出す際にどういう解決策があるか検討しました。
　今回の記事では、VPC内リソースにアクセスする構成をすると「ネットワーク経路」が変わることをお伝えしました。今回はRDS等のVPC内のリソースにアクセスする目的で構成しましたが、Security GroupやNACL、ルーティングテーブルを利用してLambda関数内のアプリ処理で通信できる先をコントロールしたいという場合にも有効活用可能な設定です。ぜひ覚えておいてください。