describe-api　AWS Lambda関数によるAWS APIの実行

# Lambda関数 describe-api


### 概要

AWS Lambda関数「**describe-api**」を作成してみたので、紹介します。

一言で言うと、**URLパラメータの組み合わせでAWSのAPIを呼び出して、結果をWebブラウザに返すLambda関数**です。
AWSのマネージメントコンソール（マネコン）に入らなくても、WebブラウザーでAWSの情報取得をできちゃいたいこと、ありませんか？
それをサーバーレスで実現するための関数です。

変更系APIと参照系APIの2種類のAPIに大別したときの **参照系API**が対象です。

### フロー図

![flow-diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204490/153b0ce8-a201-4e81-8838-0ca475f65bfd.png)

1. Webクライアント(Webブラウザ)でLambda関数のエンドポイントURLへアクセス
2. URLのパラメータによりLambda関数がAWS API(boto3)を実行
   例： ```api=ec2:describe_subnets``` → EC2 の DescribeSubnets APIを呼び出す
   S3利用時は、キャッシュが新しい場合はキャッシュを返す(図の点線)
3. Webクライアントは結果のJsonを受け取る

### 利用ケース

エンドユーザーがAWS上の情報（EC2の一覧表示など）を取得したいが、AWSのマネコンへログイン権限がない（またはログイン権限を付与したくない）、という場合があります。
API実行用のWebサーバーを立てて中継することでも実現できますが、その場合にサーバー管理の手間が発生します。
S3バケットとLambda関数だけで実現させてサーバー管理の手間も無くしたい、というケースで利用できます。
また、マネコンでは提供されていない切り口でWebブラウザに表示させたいというケースでも利用できます。

# 実装デモ

以下のWebページにアクセスして、デモを触ってみてください。
デモの中の各リンクをクリックすると、**describe-api** 関数を呼び出して結果を表示するようにしています。

[describe-api デモページ　(https://demo-aws-lambda-describe-api.s3.ap-northeast-1.amazonaws.com/web/demo-describe-api.html)](https://demo-aws-lambda-describe-api.s3.ap-northeast-1.amazonaws.com/web/demo-describe-api.html)

# GitHub

Lambda関数コードはGitHubに置いています。
[GitHub - aws-lambda-describe-api](https://github.com/hayayu0/aws-lambda-describe-api/)

- **S3バケット版**
  - Lambda関数: describe_api_with_s3.py
  - CloudFormationテンプレート: cfn-lambda-describe-api-with-s3.yaml
- **S3バケット無し版**
  - Lambda関数: describe_api.py
  - CloudFormationテンプレート: cfn-lambda-describe-api.yaml

S3バケットをキャッシュとして利用しない場合は、コードを簡易にしています。

# 特徴

Lambda関数名は ```describe-api``` としていますが、対象のAPIは、**describe**から始まるAPI 以外に **get**から始まるAPI, **list**から始まるAPI（参照系API）が対象です。

- HTTP GETメソッドを利用します: 
POSTメソッドじゃないの？と思うかもしれません。一般ユーザーでもURLの組み立てが扱いやすいように、HTTPヘッダーではなくURLパラメーターの指定だけでAPIを実行できます。
- ページネーションによる分割を意識しなくてよいです:
AWS APIの多くは、ページネーションの機能があります。それにより1回のAPIではすべての結果を得られず、複数回実行して分割した結果を結合する必要があります。この関数では、1回のURLアクセスだけで分割された結果を関数内部で結合してから取得します。
- キャッシュ機能があります（S3利用時）:
S3バケットにAPIの実行結果をキャッシュして、次回以降の高速な応答を実現します。キャッシュの有効期間を個別に指定できます。
- 複雑なJSONデータの整形・加工します:
  - **select=xxx**：APIの結果から必要項目だけに絞ります
  - **simpletag**：複雑なタグ構造をシンプル化します
  詳しくは、URLパラメータの章に記載します。
- アクセス元IPアドレスによる許可の制御をします:
環境変数 ```source_cidr_list``` により、アクセス元のIPをCIDR形式で許可します。
- API固有の変換機能があります:
  - ec2 の **describe_instances** 専用：**flatten** を付与すると配列をフラット化します
  - cloudwatch の **get_metric_widget_image** API専用：扱いやすいBase64で返します
- メタデータを返しません:
  APIの実行結果で必要なのは、そのAPIの結果データの部分です。メタデータ等は不要ですので、除去してから返します

# URLパラメータ

URLは、Lambda関数のエンドポイントの形式のドメイン（```xxxxxxx.lambda-url.リージョン名.on.aws```）式とします。

```text
https://xxxxxxx.lambda-url.your-region-name.on.aws/?
    api=service:describe_xxxxxxxx
    [&arg=【APIの引数用文字列】]
    [&region=【リージョン名】]
    [&select=【キー名1:キー名2:・・・】]
    [&cache=【キャッシュ有効秒数】]
    [&flatten]
    [&simpletag]
```

### URLパラメータの例

具体的な例はこちらにあります。

https://github.com/hayayu0/aws-lambda-describe-api/blob/main/examples.md

### api=service:describe_xxxxxx

- 必須パラメータ
- ```service``` は、IAMポリシー ReadOnlyAccess で許可されているサービス が指定可能
  例：ec2, rds, elb, s3, iam, cloudwatch (その他にも200以上あります)
- ```describe_xxxxxx``` は、各サービスの参照系のAPI
  snake_case (単語を「_」でつなぐ) で指定
- ```describe_xxxx``` の箇所は ```list_xxxx```, ```get_xxxx``` も対象

### arg=【APIの引数用文字列】

- APIによっては必須
- 値はJsonオブジェクトの { "Key" : "Value" } の形式
- URLパラメータでは最低でも ```{```→```%7B```、```}```→```%7D``` の2つのURLエンコードが必須
- ```"Key"``` は CamelCase (先頭が大文字、単語の間に文字無し)で指定します
- 例: ```api=elb:describe_instance_health``` の場合は、```arg=%7B"LoadBalancerName":"MY-LOAD-BALANCER"%7D``` のようにします
- ```Filters``` を指定する場合はやや複雑になります
  例: ```api=rds:describe_db_instances``` でSQLServer Standard Editionでフィルターするには、```arg=%7B"Filters":[%7B"Name":"engine","Values":["sqlserver-se"]%7D]%7D```

### region=【リージョン名】

- 各リージョンを指定可能
  例: us-east1, ap-northeast-1, eu-central-1 等
- デフォルト値は Lambdaの環境変数 default_region の値

### select=【キー名1:キー名2:・・・】

- APIの結果から、必要な項目だけをフィルターします
- API自体はフィルター無しで実行され、API実行結果からLambda関数内でフィルターされます
- 複数指定する場合はコロンで区切ります
  例： ```api=ec2:describe_subnets``` の場合、```select=AvailabilityZone:CidrBlock``` とすると、 各配列要素は ```{"AvailabilityZone":"xxx-xxx", "CidrBlock":"x.x.x.x/nn"}``` の項目だけ含んだJsonを返します。
- キー名にドットを指定すると、深いキー名を指定可能です。aaa.bbb.ccc のように深い階層も可能です
- キー名にドットを連続して指定すると、配列の各要素のキーでフィルターできます。
  例: ```[{"IpAddresses":[{"Ip":"1.1.1.1"}, {"Ip":"2.2.2.2"}]]``` のデータに対して ```select=IpAddresses.Ip``` を指定すると値は取れません。```select=IpAddresses..Ip``` とすると ```[{ "IpAddresses..IP":["1.1.1.1", "2.2.2.2"]}]``` を返します

### cache=秒

- S3バケットを利用する場合、APIはクライアントにデータを返しつつ、S3にキャッシュする仕様です
- 指定した秒数と比較してキャッシュが期限切れ前なら、S3に保存していたキャッシュを返します
- 値が10文字以上あると、先頭9文字より後は切り捨てられます。デフォルト値は 60 です
- S3バケットを利用するかどうかに関わらず、```cache=秒``` の数値を HTTP Cache-Control ヘッダーの ```max-age=秒``` で返します。(max-age の最大値は 604800)
- 必ず最新データが欲しい場合は、```cache=0``` とします
- S3バケットを利用する場合で、今回はキャッシュさせたくないという場合は、```cache=never``` とします

### flatten

- ```ec2:describe_instances``` 専用のオプションです
  ```ec2:describe_subnets``` など他のAPIに```flatten``` を指定しても無視されます

**```flatten```** が無い場合：

```json
{
  "Reservations": [
    {
      "Groups": [],
      "Instances": [
        {"EC2_A の属性": "EC2_A の属性の値", },
        {"EC2_B の属性": "EC2_B の属性の値", },
      ]
    },
    {
      "Groups": [],
      "Instances": [
        {"EC2_C の属性": "EC2_C の属性の値", },
      ]
    }
  ]
}
```

**```flatten```** がある場合（フラットにします）：

```json
[
  {"EC2_A の属性": "EC2_A の属性の値", },
  {"EC2_B の属性": "EC2_B の属性の値", },
  {"EC2_C の属性": "EC2_C の属性の値", }
]
```

### simpletag

- タグ情報を含む一部のAPI 専用のオプションです。APIの結果に ```Tags``` または ```TagList``` がある場合に適用されます

**```simpletag```** がない場合：

```json
[
  {
    "Key": "Name",
    "Value": "Server01"
  },
  {
    "Key": "Environment",
    "Value": "Production"
  }
]
```

**```simpletag```** がある場合（シンプルにします）：

```json
{
  "Name": "Server01",
  "Environment": "Production"
}
```

# CloudWatchメトリクスの画像取得

このLambda関数では、CloudWatchメトリクスのグラフをPNG画像で取得するAPI ```get_metric_widget_image``` に対応しています。
元のAPIではバイナリデータを受け取りますが、この関数ではBase64形式にして返されます。

呼び出す場合のURLは、以下のように組み立てます。

1. CloudWatchメトリクスで目的のグラフを描画させます
2. 発信元タブ　→　イメージAPI　→　コピー の順にクリックします ![cloudwatch-api.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204490/aed68242-750b-456a-9e9c-4e305e298c3f.png)
3. 上述したように、```{```→```%7B```、```}```→```%7D``` のように置換します
4. さらに、```"```→```%5C"``` のように、バックスラッシュをエンコードした文字を ```"``` の前に付与します
  上記画像をエンコードした例:
  ```%7B%5C"view%5C":%5C"timeSeries%5C", ～中略～ %5C"end%5C":%5C"P0D%5C"%7D```
5. 出来上がった文字列を ```https://xxxx.lambda-url.your-region-name.on.aws/api=cloudwatch:get_metric_widget_image&arg=%7B%22MetricWidget%22:%22``` と ```%22%7D``` の間に挟んで、結合した文字列がURLになります

出来上がったURLにアクセスすると、{"MetricWidgetImage": "iVBOR～～～～～～～～" } のJsonが返ってきます。
```iVBOR～～～～～～～～``` はPNG画像のBase64形式です。
これを ```<img src="data:image/png;base64,iVBOR～～～～～～～～" />``` のようなimgタグをJavascriptで動的に作成して、画像をブラウザに表示させることができます。

# Lambda関数の環境変数

各環境変数が定義できます。

- **s3_bucket**:
  S3バケット名を指定すると、一度取得したJsonをS3に保管してキャッシュとして使用するようになります
- **default_region**: 
  region= のURLパラメータを付けない場合の、デフォルトのリージョンです。この環境変数も指定せずURLパラメータも付けない場合は、```us-east-1``` となります
- **source_cidr_list**:
  アクセス元のIPアドレスをCIDR形式で許可します。デフォルトは無制限です。```0.0.0.0/0, ::/0``` とすると無制限になります。空白区切りで複数指定できます
- **default_cache**:
  S3のキャッシュ利用時、URLパラメータに cache=数字 を付けない場合、この環境変数の数字がキャッシュ有効秒となります
- **remove_key_list**:
  APIを実行した後に取得された結果には、メタデータなどを含んでいます。この関数では、この環境変数で指定したものを除去します。値は ```ResponseMetadata Marker NextToken nextToken IsTruncated MaxResults``` としておけばよいでしょう

# エラーに関すること

- URLパラメータに ```{``` と ```}``` をエンコードせずにそのままの文字で呼び出すと、Lambda関数まで到達できません。
- api の不足、存在しないAPI名の指定、変更系APIの指定、arg のミス等、必須パラメータの不足があると { "Error_Message": "xxxxxxx" } の Jsonが返ります。
  例：```{ "Error_Message": "Content size limit exceeded." }```
- Lambda関数エンドポイント経由でのアクセスの制約で 処理に59秒以上かかると失敗します
- Lambda関数の制約の関係で、この関数では結果が 5.75MiB を超えていると失敗します。結果のデータが巨大になりそうな場合は、```select``` を活用することでエラーを防げます

# あとがき

関数自体は2019年に初版を作成し、徐々に機能拡張しながら、AWSインフラ運用管理に役立ててきました。
2022年には今の機能を実装済みでしたが、生成AIでコードをブラッシュアップしましたので、記事として公開に踏み切りました。

この関数をベースにクライアントサイド(Javascript)で、幅広いツールの作成に活用できます。
ぜひ試してみてください。

正直、この関数だけだと便利なサイトを作成したとは言えませんね。このLambda関数を活用した便利なWebサービス例は、別記事で紹介したいと思います。