![]() |
はじめに
こんにちは。SCSKのふくちーぬです。
今回は、自動で Amazon API Gateway REST API 定義ファイルのバックアップを取得してみました。REST API には、API エンドポイントの情報を出力するエクスポート機能が備わっています。またエクスポート機能は、マネジメントコンソール・AWS CLIやSDKでサポートされています。
このAPI定義ファイルは、クライアントサイドからAPI呼び出しを利用した開発をするために、配布・提供することが一般的です。そのため最新のAPI定義ファイルを保管しておくことは重要なのです。しかし、みなさんAPIのデプロイ毎にポチポチと面倒なエクスポート処理をしていないでしょうか?
SDKを使用したLambdaを活用することで、自動でAPI定義ファイルのバックアップを取得してみましょう。
環境準備編
ここでは、サンプルであるAPI Gateway + Lambdaをデプロイしておきます。既にAWSアカウント上で、デプロイ済みのAPI Gatewayが存在する場合は本作業は飛ばしていただいても結構です。
以前紹介した記事に、API Gateway + LambdaのCloudFormationテンプレートを用意しているのでご利用ください。
CloudFormationテンプレートをデプロイできたら環境準備完了です。
REST APIのエクスポート機能について
REST APIでステージへデプロイが完了すると、以下のようにAPI定義ファイルをエクスポートできる状態となります。
オプション機能として、API Gateway固有の設定(Lambda等)も含めてエクスポートすることも可能です。
またこのREST APIの定義ファイルは、標準規格であるOpen API仕様に基づいて作成されています。
今回は、以下の設定でエクスポートしてみます。
API仕様タイプ | 形式 | 拡張機能 |
Open API 3 | YAML | API Gateway拡張機能ありでエクスポート |
検証①日時でバックアップを取得してみる
構成図
サンプルAPIとしてAPI Gateway + Lambdaがデプロイ済みです。
- EventBridgeにて日時起動します。
- トリガーが動いたことで、Lambdaが起動します。対象は、サンプルAPIを対象にエクスポート処理を実施します。
- Lambdaにて取得したAPI定義ファイルをS3に保存します。
ソリューションのデプロイ
以下のCloudFormationテンプレートをデプロイしてください。
AWSTemplateFormatVersion: 2010-09-09
Parameters:
ResourceName:
Type: String
APIId:
Type: String
APIStage:
Type: String
Resources:
# ------------------------------------------------------------#
# IAM Policy IAM Role
# ------------------------------------------------------------#
LambdaPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub ${ResourceName}-lambda-policy
Description: IAM Managed Policy with S3 PUT and APIGateway GET Access
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- 's3:PutObject'
- 'apigateway:GET'
Resource:
- '*'
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${ResourceName}-lambda-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- !GetAtt LambdaPolicy.PolicyArn
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
MyS3Bucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
BucketName: !Sub ${ResourceName}-${AWS::AccountId}-bucket
# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------#
APIExportFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub ${ResourceName}-lambda-function
Role: !GetAtt LambdaRole.Arn
Runtime: python3.11
Handler: index.lambda_handler
Environment:
Variables:
RESTAPI_ID: !Ref APIId
RESTAPI_STAGE: !Ref APIStage
S3_BUCKET_NAME: !Sub ${ResourceName}-${AWS::AccountId}-bucket
Code:
ZipFile: !Sub |
import boto3
import tempfile
import os
from datetime import datetime, timedelta
import shutil
# APIGatewayとS3の指定
client_apigateway = boto3.client('apigateway')
client_s3 = boto3.client('s3')
def lambda_handler(event, context):
try:
response = client_apigateway.get_export(
restApiId= os.environ['RESTAPI_ID'],
stageName= os.environ['RESTAPI_STAGE'],
exportType= 'oas30',
parameters= {
"extensions": "apigateway"
},
accepts= 'application/yaml'
)
# ファイルを保存する一時ディレクトリのパスを作成
temp_dir = '/tmp/swagger'
os.makedirs(temp_dir, exist_ok=True)
# 変数の代入
# 現在のUTC時間を取得
utc_now = datetime.utcnow()
# UTCから日本時間に変換(9時間を加算)
jst_now = utc_now + timedelta(hours=9)
# 日付と時刻
jst_time = jst_now.strftime("%Y-%m-%d_%H%M%S")
# 日付のみ
jst_date = jst_now.strftime("%Y-%m-%d")
# S3バケット名
s3_bucket_name = os.environ['S3_BUCKET_NAME']
s3_object_key = 'apigateway/' + jst_date + '/' + jst_time + '_' + 'apigateway' + '_' + os.environ['RESTAPI_ID'] + '_' + os.environ['RESTAPI_STAGE'] + '.yaml'
# YAMLファイルを一時ディレクトリに書き込み
yaml_file_path = os.path.join(temp_dir, 'api_gateway.yaml')
with open(yaml_file_path, 'w') as file:
file.write(response['body'].read().decode('utf-8'))
# S3にファイルをアップロード
client_s3.upload_file(yaml_file_path, s3_bucket_name , s3_object_key)
print('API Gateway YAML file uploaded to S3 successfully.')
except Exception as e:
# 例外が発生した場合の処理
print(f"An error occurred: {str(e)}")
return {
'statusCode': 500,
'body': f'Error: {str(e)}'
}
finally:
# 一時ディレクトリを削除
shutil.rmtree(temp_dir)
return {
'statusCode': 200,
'body': 'API Gateway YAML file uploaded to S3 successfully.'
}
# ------------------------------------------------------------#
# EventBridge
# ------------------------------------------------------------#
EventBridgeRule:
Type: AWS::Events::Rule
Properties:
EventBusName: default
Name: !Sub ${ResourceName}-eventbridge-rule
ScheduleExpression: cron(0 15 * * ? *)
State: ENABLED
Targets:
- Arn: !GetAtt APIExportFunction.Arn
Id: Lambda
PermissionForEventsToInvokeLambda:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Sub ${ResourceName}-lambda-function
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt 'EventBridgeRule.Arn'
IAM Policy IAM Role
簡単にCloudFormationテンプレートの説明をします。
Lambdaに付与するIAM権限として、以下の2つを指定しています。
- ‘s3:PutObject’(S3にファイルをアップロードするための権限)
- ‘apigateway:GET’(API定義ファイルをエクスポートするための権限)
S3
スタック削除後にAPI定義ファイルが格納されたS3が削除されないように、削除ポリシーにて”Retain”を指定しています。
Lambda
大きく2つの操作を行っています。
- 指定したREST APIのID及びデプロイされたステージに対して、API定義ファイルのエクスポート処理をする。
- 指定したS3バケットにAPI定義ファイルを格納する
EventBridge
日本時間で、00時00分に起動するよう設定しています。
またEventBridgeがLambdaをターゲットとして起動できるようリソースポリシーを設定しています。
バックアップの確認
以下のように00時00分に指定のS3に、バックアップが取得できていることがわかります。
API定義ファイルを確認してみると、1つのGETメソッドの情報が記述されています。
openapi: "3.0.1"
info:
title: "sampleapi-20231130-apigateway"
version: "2023-11-30T14:41:36Z"
servers:
- url: "https://jy67uff70h.execute-api.ap-northeast-1.amazonaws.com/{basePath}"
variables:
basePath:
default: "dev"
paths:
/utcnow:
get:
x-amazon-apigateway-integration:
type: "aws_proxy"
httpMethod: "POST"
uri: "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:<awsアカウントid>:function:sampleapi-20231130-lambda-function-utcnow/invocations"
passthroughBehavior: "when_no_match"
components: {}
検証②デプロイ毎にバックアップを取得してみる
次は、ステージへのデプロイ(更新)ごとにバックアップを取得するパターンをみていきます。REST APIのエンドポイントの更新が行われる度に、API定義ファイルも最新版に保持しましょう。
構成図
基本的には、①と同様となります。イベント実行をしている箇所のみ異なります。
- EventBridgeにて対象APIのデプロイ毎に起動します。
- トリガーが動いたことで、Lambdaが起動します。対象は、サンプルAPIを対象にエクスポート処理を実施します。
- Lambdaにて取得したAPI定義ファイルをS3に保存します。
ソリューションのデプロイ
以下のCloudFormationテンプレートをデプロイしてください。
基本的には、①のテンプレートと同様です。EventBridgeにてイベント実行している箇所のみ異なります。
AWSTemplateFormatVersion: 2010-09-09
Parameters:
ResourceName:
Type: String
APIId:
Type: String
APIStage:
Type: String
Resources:
# ------------------------------------------------------------#
# IAM Policy IAM Role
# ------------------------------------------------------------#
LambdaPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub ${ResourceName}-lambda-policy
Description: IAM Managed Policy with S3 PUT and APIGateway GET Access
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- 's3:PutObject'
- 'apigateway:GET'
Resource:
- '*'
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${ResourceName}-lambda-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- !GetAtt LambdaPolicy.PolicyArn
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
MyS3Bucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
BucketName: !Sub ${ResourceName}-${AWS::AccountId}-bucket
# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------#
APIExportFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub ${ResourceName}-lambda-function
Role: !GetAtt LambdaRole.Arn
Runtime: python3.11
Handler: index.lambda_handler
Environment:
Variables:
RESTAPI_ID: !Ref APIId
RESTAPI_STAGE: !Ref APIStage
S3_BUCKET_NAME: !Sub ${ResourceName}-${AWS::AccountId}-bucket
Code:
ZipFile: !Sub |
import boto3
import tempfile
import os
from datetime import datetime, timedelta
import shutil
# APIGatewayとS3の指定
client_apigateway = boto3.client('apigateway')
client_s3 = boto3.client('s3')
def lambda_handler(event, context):
try:
response = client_apigateway.get_export(
restApiId= os.environ['RESTAPI_ID'],
stageName= os.environ['RESTAPI_STAGE'],
exportType= 'oas30',
parameters= {
"extensions": "apigateway"
},
accepts= 'application/yaml'
)
# ファイルを保存する一時ディレクトリのパスを作成
temp_dir = '/tmp/swagger'
os.makedirs(temp_dir, exist_ok=True)
# 変数の代入
# 現在のUTC時間を取得
utc_now = datetime.utcnow()
# UTCから日本時間に変換(9時間を加算)
jst_now = utc_now + timedelta(hours=9)
# 日付と時刻
jst_time = jst_now.strftime("%Y-%m-%d_%H%M%S")
# 日付のみ
jst_date = jst_now.strftime("%Y-%m-%d")
# S3バケット名
s3_bucket_name = os.environ['S3_BUCKET_NAME']
s3_object_key = 'apigateway/' + jst_date + '/' + jst_time + '_' + 'apigateway' + '_' + os.environ['RESTAPI_ID'] + '_' + os.environ['RESTAPI_STAGE'] + '.yaml'
# YAMLファイルを一時ディレクトリに書き込み
yaml_file_path = os.path.join(temp_dir, 'api_gateway.yaml')
with open(yaml_file_path, 'w') as file:
file.write(response['body'].read().decode('utf-8'))
# S3にファイルをアップロード
client_s3.upload_file(yaml_file_path, s3_bucket_name , s3_object_key)
print('API Gateway YAML file uploaded to S3 successfully.')
except Exception as e:
# 例外が発生した場合の処理
print(f"An error occurred: {str(e)}")
return {
'statusCode': 500,
'body': f'Error: {str(e)}'
}
finally:
# 一時ディレクトリを削除
shutil.rmtree(temp_dir)
return {
'statusCode': 200,
'body': 'API Gateway YAML file uploaded to S3 successfully.'
}
# ------------------------------------------------------------#
# EventBridge
# ------------------------------------------------------------#
EventBridgeRule:
Type: AWS::Events::Rule
Properties:
EventBusName: default
Name: !Sub ${ResourceName}-eventbridge-rule
EventPattern:
source:
- "aws.apigateway"
detail:
eventSource:
- "apigateway.amazonaws.com"
eventName:
- "CreateDeployment"
requestParameters:
restApiId:
- !Ref APIId
State: ENABLED
Targets:
- Arn: !GetAtt APIExportFunction.Arn
Id: Lambda
PermissionForEventsToInvokeLambda:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Sub ${ResourceName}-lambda-function
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt 'EventBridgeRule.Arn'
Event Bridge
対象APIにてデプロイが行われるとEventBridgeが起動するようイベントパターンを設定しています。
ステージへの再デプロイ
今回は、自動デプロイ機能を利用してREST APIを自動更新します。もしくは、ご自身でREST APIに何らかの変更を加えた上で、ステージへデプロイしていただいても構いません。
以下の記事で紹介しているので、是非こちらのCloudFormationテンプレートを利用してデプロイしてください。
今回は、以下構成のようにサンプルAPIのメソッドを1つ追加しています。
バックアップの確認
デプロイが行われると指定のS3に、バックアップが取得できていることがわかります。
API定義ファイルにも、2つのメソッド情報がきちんと記述されていますね。
openapi: "3.0.1"
info:
title: "sampleapi-20231130-apigateway"
version: "2023-12-01T15:53:53Z"
servers:
- url: "https://jy67uff70h.execute-api.ap-northeast-1.amazonaws.com/{basePath}"
variables:
basePath:
default: "dev"
paths:
/jstnow:
get:
x-amazon-apigateway-integration:
httpMethod: "POST"
uri: "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:<awsアカウントid>:function:sampleapi-20231130-lambda-function-jstnow/invocations"
passthroughBehavior: "when_no_match"
type: "aws_proxy"
/utcnow:
get:
x-amazon-apigateway-integration:
httpMethod: "POST"
uri: "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:<awsアカウントid>:function:sampleapi-20231130-lambda-function-utcnow/invocations"
passthroughBehavior: "when_no_match"
type: "aws_proxy"
components: {}
まとめ
いかがだったでしょうか。自動でREST APIのAPI定義ファイルのバックアップを取得してみました。
API Gatewayを構築したら終わりではなく、必ずAPIの利用者は存在しますのでAPI定義ファイルの面倒まで見てあげることが大切ですね。
ご利用の際は、要件に応じてカスタマイズ等していただければと思います。
本記事が皆様のお役にたてば幸いです。
ではサウナラ~🔥