自動で Amazon API Gateway REST API 定義ファイルのバックアップを取得してみた

 

本記事は 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がデプロイ済みです。

  1. EventBridgeにて日時起動します。
  2. トリガーが動いたことで、Lambdaが起動します。対象は、サンプルAPIを対象にエクスポート処理を実施します。
  3. 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定義ファイルも最新版に保持しましょう。

構成図

基本的には、①と同様となります。イベント実行をしている箇所のみ異なります。

  1. EventBridgeにて対象APIのデプロイ毎に起動します。
  2. トリガーが動いたことで、Lambdaが起動します。対象は、サンプルAPIを対象にエクスポート処理を実施します。
  3. 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定義ファイルの面倒まで見てあげることが大切ですね。

ご利用の際は、要件に応じてカスタマイズ等していただければと思います。

本記事が皆様のお役にたてば幸いです。

ではサウナラ~🔥

タイトルとURLをコピーしました