FJCT Tech blog

富士通クラウドテクノロジーズ公式エンジニアブログです

富士通クラウドテクノロジーズ

FJCT/Tech blog

Data-driven NIFCLOUD SDK for Python の裏側

この記事は 富士通クラウドテクノロジーズ Advent Calendar 2018 の 16 日目の記事です。 昨日は YoshidaY さんの gRPCのシナリオテスト用コードを生成するprotocプラグインを作ってみた でした。

こんにちは! FJCT で主に PaaS 系サービスの開発・運用をしている id:alice02san です。 今日は私が関わっているプロダクトの一つである、 Data-driven NIFCLOUD SDK for Python (Developer Preview) の裏側について少しお話しようと思います。

NIFCLOUD SDK for Python

2018 年 4 月 にリリースされた、NIFCLOUD SDK for Python (Developer Preview) をご存知でしょうか? 従来、ニフクラSDK として、 Java SDK が主なものとして存在していましたが、そこに Python SDK が加わった形となります。 (まだ Developer Preview 版ですが…)

この Python SDK の特徴としては下記が挙げられます。

  • データ駆動型 SDK
    • 後ほど詳しく説明しますが、 SDK 内にコミットされている、 JSON ファイルから SDK が自動生成されています。
  • 1 つの SDK で複数のサービスに対応
  • CLI ツールが同封
    • CLI ツール nifcloud-debugcli が同封されており、インストールするだけでコマンドラインから簡単に NIFCLOUD の API をリクエストすることが可能です。

使い方等は ニフクラ SDK for Python (Developer Preview) のご紹介リポジトリの README簡単なドキュメント (Read the Docs) に記載されているので、適宜御覧ください。

さて、この NIFCLOUD SDK for Python ですが、ソースコードを見てみると、とても実装が少ないことがわかります。基本的には AWSPython SDK である、 botocore をラップしたものになっているからです。 勘が鋭い方はお気づきかもしれませんが、ニフクラの API リファレンスを見ると AWSAPI 仕様ととても似ていることがわかります。特にリクエスト形式や認証系はかなり互換性があります。この互換性を利用し、 AWS SDK の仕組みに乗っかることで、 NIFCLOUD SDK for Python が実装されています。

そこで、今回はこの SDK の裏側の仕組みについて少し解説できればなと思います!!

AWS SDK の仕組み

それでは、 AWS SDK の仕組みについて、AWSPython SDK である botocoreソースコードを読むことで調べてみます。あくまでも私がソースコードをざっと読んだ感じの憶測になります。笑

全体的にソースコードを眺めてみると、 https://github.com/boto/botocore/blob/develop/botocore/loaders.py あたりで、https://github.com/boto/botocore/tree/develop/botocore/data 配下に存在する model (実態は json ファイル) を読み込んでいるようです。 では、この model ファイルの中を軽く眺めてみます。サンプルとして、 ec2 の service-2.json の中身を見てみました。

{
  "version":"2.0",
  "metadata":{
    "apiVersion":"2016-11-15",
    "endpointPrefix":"ec2",
    "protocol":"ec2",
    "serviceAbbreviation":"Amazon EC2",
    "serviceFullName":"Amazon Elastic Compute Cloud",
    "serviceId":"EC2",
    "signatureVersion":"v4",
    "uid":"ec2-2016-11-15",
    "xmlNamespace":"http://ec2.amazonaws.com/doc/2016-11-15"
  },
  "operations":{
    "AcceptReservedInstancesExchangeQuote":{
      "name":"AcceptReservedInstancesExchangeQuote",
      "http":{
        "method":"POST",
        "requestUri":"/"
      },
      "input":{"shape":"AcceptReservedInstancesExchangeQuoteRequest"},
      "output":{"shape":"AcceptReservedInstancesExchangeQuoteResult"},
      "documentation":"<p>Accepts the Convertible Reserved Instance exchange quote described in the <a>GetReservedInstancesExchangeQuote</a> call.</p>"
    },

    ...

  "shapes":{
    "AcceptReservedInstancesExchangeQuoteRequest":{
      "type":"structure",
      "required":["ReservedInstanceIds"],
      "members":{
        "DryRun":{
          "shape":"Boolean",
          "documentation":"<p>Checks whether you have the required permissions for the action, without actually making the request, and provides an error response. If you have the required permissions, the error response is <code>DryRunOperation</code>. Otherwise, it is <code>UnauthorizedOperation</code>.</p>"
        },
        "ReservedInstanceIds":{
          "shape":"ReservedInstanceIdSet",
          "documentation":"<p>The IDs of the Convertible Reserved Instances to exchange for another Convertible Reserved Instance of the same or higher value.</p>",
          "locationName":"ReservedInstanceId"
        },
        "TargetConfigurations":{
          "shape":"TargetConfigurationRequestSet",
          "documentation":"<p>The configuration of the target Convertible Reserved Instance to exchange for your current Convertible Reserved Instances.</p>",
          "locationName":"TargetConfiguration"
        }
      },
      "documentation":"<p>Contains the parameters for accepting the quote.</p>"
    },
    "AcceptReservedInstancesExchangeQuoteResult":{
      "type":"structure",
      "members":{
        "ExchangeId":{
          "shape":"String",
          "documentation":"<p>The ID of the successful exchange.</p>",
          "locationName":"exchangeId"
        }
      },
      "documentation":"<p>The result of the exchange and whether it was <code>successful</code>.</p>"
    },

    ...

各 root フィールドの役割はざっと以下の感じだと思われます。

  • metadata
    • 各サービスごとに異なる endpoint, signature version の定義が書かれているみたい。
  • operations
    • API のアクション、リクエスト、レスポンスの形式 (実態は shapes に定義されている) の定義が書かれているみたい。
    • 例えば EC2 のインスタンス情報を取得する DescribeInstances など。
  • shapes
    • operations に定義されるリクエスト、レスポンスの形式の実際の中身が書かれているみたい。
    • 例えば DescribeInstances という API に対応する shapes には、リクエストに必要なパラメータやレスポンスボディの構造書かれているみたい。

そして、この読み込んだ model から https://github.com/boto/botocore/blob/develop/botocore/client.py あたりで client を動的に生成しているみたいです。ここで生成される client のメソッドを呼び出すことで、各 API のリクエストができるみたいです。

さて、とてもざっくりした解説でしたが、 SDK のキモがこの json 形式の model ファイルであることが伝わっていれば大丈夫です。

ちなみに他言語の SDK も簡単に調べてみた感じ基本的には2つのタイプに分かれていて、 botocore のように import 後に model から client を動的に生成するもの (client 動的生成型)、もう 1 つが model からテンプレートエンジンを用いてソースコードを生成し、生成したソースコードを import するタイプ (ソースコード生成型) があるみたいでした。

AWSSDK はこのように各サービスの API に関する情報を model に落とし込みデータ駆動にすることで、多言語 SDK の対応を容易にしているみたいですね!!すごい!!

botocore をラップした NIFCLOUD SDK for Python

さて、前述のとおり、 NIFCLOUD SDK for Python は、 AWS SDK (botocore) の仕組みを利用して実装されています。

NIFCLOUD 向けの model ファイルは https://github.com/nifcloud/nifcloud-sdk-python/tree/master/nifcloud/data に配置されており、この model から動的に client を生成しています。なお、コミットされている model ファイルは、別で存在するマスターデータから自動的に生成されています。

ということもあり、 NIFCLOUD SDK for Python の使い方はほぼ botocore と同じものになっています。

また aws cli が botocore に依存していることを利用して、 aws cli 互換の nifcloud-debugcli というコマンドラインツールも同時に公開することができたというわけになります。

nifcloud-sdk-go

そんなこんなで、 AWS SDK の仕組みをまたまたお借りして、 nifcloud-sdk-go というのを作ってみました。 まだ、個人の趣味レベルで作ったものなので完成度が低いものですが、いつかは公式 SDK としてリリースできればなーと考えています。特に Golang SDK があると terraform 対応や mackerel-plugin などがとても書きやすくなるので、頑張って進めていきたいなーと思っています。

まとめ

この記事では NIFCLOUD SDK for Python (Developer Preview) の仕組みについて簡単に解説してみました。SDK を使う分にはこういった仕組みは気にしなくても大丈夫ですが、同一サービスの SDK を多言語対応させたい場合とかで考え方が参考になるんじゃないかなーと思いました。

また、 NIFCLOUD SDK for Python を用いることで、構築・運用作業の自動化など、 NIFCLOUD をより便利に使うことができるようになると思います。機会がありましたら、ぜひ使っていただければと思います。

明日は abej さんが「海外ITカンファレンス動画のすすめ」について書いてくれるみたいです。お楽しみに!!