本記事は TechHarmony Advent Calendar 12/5付の記事です。 |
はじめに
こんにちは。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定義ファイルの面倒まで見てあげることが大切ですね。
ご利用の際は、要件に応じてカスタマイズ等していただければと思います。
本記事が皆様のお役にたてば幸いです。
ではサウナラ~🔥