🐭

SmithyでAPIリファレンス作成してみよう

2021/12/14に公開

はじめに

みなさんはじめまして、ニフクラのエンジニアをやっております@seumoと申します🐭

本記事は富士通クラウドテクノロジーズ Advent Calendar 2021の14日目の記事です。

13日目は @tunakyonn🤴のCDK for Terraform をニフクラ provider で試してみたという記事でした。
得意なプログラミング言語でクラウドのリソース管理を自動化できるのはとても便利ですね、私も今度Javaで試してみようかと思います。

Smithyとは

SmithyはAmazonが開発したIDL(インターフェース記述言語)に基づくツールセットであり、プログラミング言語やプロトコルに依存せずサービスのインターフェースを定義することができ、さらにその定義から複数のプログラミング言語のコードを自動生成できたりするものです。

SmithyでAPIリファレンスを作成してみる

それでは早速公開されているSmithyのドキュメントを見ながらAPIリファレンスを作成してみようと思います!
今回はニフクラのKubernetes Service HatobaのAPIの一部をSmithyで定義したらどうなるのか、検証してみます。

サービスの定義

  • SmithyはShapeと呼ばれるモデルの定義によって構成されていきます
  • Shapeとは名前付きオブジェクトのようなものでいくつかの型があり、まずはじめに定義するのがサービスのShapeとなります

Shapeは以下のフォーマットで記述することができます。

{型} {名前} { 
  {プロパティ名}: {値}
}

Kubernetes Service Hatobaのサービスを定義してみます。

service KubernetesServiceHatoba {
    version: "v1"
} 

上記例では、service型のshapeをKubernetesServiceHatobaという名前で定義し、プロパティとしてversionにv1を設定したという記述になります。

オペレーションの定義

  • 次にそのサービスではどんなオペレーションが可能なのかを定義します
  • 今回はKubernetes Service Hatobaが提供している機能の1つであるCreateFirewallGroupを例に定義していきます
service KubernetesServiceHatoba {
    version: "v1",
    operations: [
        CreateFirewallGroup,
    ],
}

operation CreateFirewallGroup {
    input: CreateFirewallGroupRequest,
    output: CreateFirewallGroupResult,
}
  • 先ほどのサービスShapeにoperationsというプロパティを追加しました
  • さらにoperation型のShapeを新しく定義します
  • operation型のShapeにはAPI操作の入力と出力のShapeを定義することができます
  • まだ入力と出力のShapeの中身を書いていないため次の項で記述していきます

リソースの定義

  • まずはinputのCreateFirewallGroupRequestのリソースから定義してみます
  • structure型のShapeを用いて定義します
structure CreateFirewallGroupRequest {
    FirewallGroup: RequestFirewallGroup,
}

structure RequestFirewallGroup {
    Name: String,
    Description: String,
}
  • CreateFirewallGroupのAPIでは、FirewallGroupをinputとして指定します

  • FirewallGroupのリソースをRequestFirewallGroupという名前のstructure型のShapeで定義し、NameとDescriptionという2つのstring型プロパティをもっているという記述になります

  • 次にoutputのCreateFirewallGroupResultを定義してみます

structure CreateFirewallGroupResult {
    FirewallGroup: FirewallGroup,
}

structure FirewallGroup {
    Name: String,
    Description: String,
    Rules: ListOfRules,
}

list ListOfRules {
    member: Rules,
}

structure Rules {
    Description: String,
    Direction: String,
    Status: String,
    CidrIp: String,
    ToPort: Integer,
    Id: String,
    Protocol: String,
    FromPort: Integer,
}
  • outputとしてFirewallGroupが返却されるためそれを定義します
  • FirewallGroupにはRulesという要素があり配列になっています
  • 配列はlist型のShapeで定義し、list型のShapeはmemberプロパティにlistの要素を指定することができます、ここではRulesを指定しています

トレイトの定義

  • CreateFirewallGroupというオペレーションに対してShapeの定義が一通り完了しました
  • 次はこのShapeにトレイトを付けていきます
  • Smithyではトレイトを利用してShapeに様々な追加情報を付与することができます
  • Kubernetes Service HatobaのAPIはHTTPでリクエストできるAPIなので、HTTP binding traitsを付与してみます
  • HTTP binding traitsはoperationのShapeに指定できます
@http(method: "POST", uri: "/v1/firewallGroups" , code: 201)
operation CreateFirewallGroup {
    input: CreateFirewallGroupRequest,
    output: CreateFirewallGroupResult,
}
  • トレイトは @トレイト名(プロパティ) で指定します

  • 上記例ではCreateFirewallGroupのAPIはPOSTメソッドで/v1/firewallGroupsというuriでリクエストができるということを意味し、また正常系のステータスコードは201であるという定義となっております

  • 次にKubernetes Service HatobaはJSON形式でデータをやりとりするREST APIであるため、リソースをシリアライズするためのProtocol traitsをShapeに定義します

structure CreateFirewallGroupRequest {
    @required
    @jsonName("firewallGroup")
    FirewallGroup: RequestFirewallGroup,
}
  • 上記例ではFirewallGroupという名前でプロパティを定義していますが、実際のAPIのインターフェースでのJSONのキー値はfirewallGroupと小文字から始まるのでそれを @jsonName トレイトで指定することができます

  • また、Shapeの各プロパティにはConstraint traitsを指定することができます。

  • @required はその名の通り必須パラメーターであることを意味します

  • Constraint traitsは他にもlengthやrange、pattern、enumなど細かく値の形式を指定することができるものもあるので必要に応じて設定します

  • 最後に応用編として、AWSの特別トレイトについて紹介します

  • SmithyはAmazonが開発したものなので、AWSのサービスが提供しているAPIで利用できるトレイトがいくつかあります

use aws.protocols#restJson1
use aws.auth#sigv4

@restJson1
@sigv4(name: "hatoba")
service KubernetesServiceHatoba {
    version: "v1",
    operations: [
        CreateFirewallGroup,
    ],
}
  • AWSの特別トレイトを利用するにはまずuseで宣言します

  • AWSのAPIはサービスによって様々な呼び出し方がありますが、lambdaのようなペイロードがJSONでRest形式のものを restJson1 というプロトコルと呼んでいるようです

  • Kubernetes Service Hatobaも同じ形式なのでこれを指定します。

  • また、AWSのAPIはシグネチャーによって認証する形式となっておりサービスに sigv4 トレイトを指定することでこれを表すことができます

  • ニフクラのKubernetes Service HatobaのAPIの認証は、AWSのシグネチャー認証と互換性があるため同じように指定することができます。

完成形

  • サービス、オペレーション、リソースを定義しトレイトを付与することで一通りAPIリファレンスが完成しました、全体は以下のようになります。
kubernetes-service-hatoba.smithy
namespace KubernetesServiceHatoba

use aws.protocols#restJson1
use aws.auth#sigv4

@restJson1
@sigv4(name: "hatoba")
service KubernetesServiceHatoba {
    version: "v1",
    operations: [
        CreateFirewallGroup,
    ],
}

@http(method: "POST", uri: "/v1/firewallGroups" , code: 201)
operation CreateFirewallGroup {
    input: CreateFirewallGroupRequest,
    output: CreateFirewallGroupResult,
}

structure CreateFirewallGroupRequest {
    @required
    @jsonName("firewallGroup")
    FirewallGroup: RequestFirewallGroup,
}

structure CreateFirewallGroupResult {
    @jsonName("firewallGroup")
    FirewallGroup: FirewallGroup,
}

structure RequestFirewallGroup {
    @required
    @jsonName("name")
    Name: String,
    @jsonName("description")
    Description: String,
}

structure FirewallGroup {
    @jsonName("name")
    Name: String,
    @jsonName("rules")
    Rules: ListOfRules,
    @jsonName("description")
    Description: String,
}

list ListOfRules {
    member: Rules,
}

structure Rules {
    @jsonName("description")
    Description: String,
    @jsonName("direction")
    Direction: String,
    @jsonName("status")
    Status: String,
    @jsonName("cidrIp")
    CidrIp: String,
    @jsonName("toPort")
    ToPort: Integer,
    @jsonName("id")
    Id: String,
    @jsonName("protocol")
    Protocol: String,
    @jsonName("fromPort")
    FromPort: Integer,
}

APIリファレンスをビルドしてみる

  • モデルができたのでこれをビルドしてみます!
  • モデルをビルドするためにはSmithy Gradle Pluginを利用します
  • まずはsmithy-build.jsonファイルを以下のように作成します
smithy-build.json
{
    "version": "1.0"
}
  • 次にbuild.gradle.ktsファイルを以下のように作成します
build.gradle.kts
plugins {
    id("software.amazon.smithy").version("0.6.0")
}

configure<software.amazon.smithy.gradle.SmithyExtension> {}

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation("software.amazon.smithy:smithy-model:1.15.0")
    implementation("software.amazon.smithy:smithy-aws-traits:1.15.0")
}
  • 完成形のモデルを model/kubernetes-service-hatoba.smithy パスにコピーしてから gradle build コマンドを実行することでビルドが完了します

コードを自動生成してみる

  • ビルドができるようになったので今度はSmithy Typescriptを利用してTypeScriptのコードを生成してみます
  • まずsmithy-build.jsonを以下のように書き換えます
smithy-build.json
{
    "version": "1.0",
    "plugins": {
        "typescript-codegen": {
            "service": "KubernetesServiceHatoba#KubernetesServiceHatoba",
            "package": "KubernetesServiceHatoba",
            "packageVersion": "1.0.0"
        }
    }
}
  • Smithy TypescriptのREADMEに記載の手順を参考にmaven localにpublishします
  • build.gradle.ktsのdependenciesにsmithy-typescript-codegenを追加します
build.gradle.kts
plugins {
    id("software.amazon.smithy").version("0.6.0")
}

configure<software.amazon.smithy.gradle.SmithyExtension> {}

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation("software.amazon.smithy:smithy-model:1.15.0")
    implementation("software.amazon.smithy:smithy-aws-traits:1.15.0")
    implementation("software.amazon.smithy.typescript:smithy-typescript-codegen:0.8.0")
}
  • gradle build コマンドを実行することで以下のようなAPIクライアントのコードが自動生成されました!
└── typescript-codegen
    ├── jest.config.js
    ├── package.json
    ├── src
    │   ├── commands
    │   │   ├── CreateFirewallGroupCommand.ts
    │   │   └── index.ts
    │   ├── index.ts
    │   ├── KubernetesServiceHatobaClient.ts
    │   ├── KubernetesServiceHatoba.ts
    │   ├── models
    │   │   ├── index.ts
    │   │   └── models_0.ts
    │   ├── runtimeConfig.browser.ts
    │   ├── runtimeConfig.native.ts
    │   ├── runtimeConfig.shared.ts
    │   └── runtimeConfig.ts
    ├── tsconfig.es.json
    ├── tsconfig.json
    └── tsconfig.types.json
  • コードの自動生成についてはTypeScriptの他にGoRustにも対応しているみたいですね
  • いずれもまだv0.x系なので今後のエンハンスによってはインターフェースや仕様変更がある点は注意です

OpenAPIに変換してみる

  • Smithyはプロトコルに依存しないIDLですが、RestFulのAPIリファレンスを記述する場合はOpenAPIを利用している方も多いと思います
  • なんとこのSmithyはOpenAPIに自動変換することも可能なので早速試してみます
  • まずsmithy-build.jsonに以下のようにpluginsの設定を追加します
smithy-build.json
{
    "version": "1.0",
    "plugins": {
        "openapi": {
            "service": "KubernetesServiceHatoba#KubernetesServiceHatoba",
            "protocol": "aws.protocols#restJson1"
        },
        "typescript-codegen": {
            "service": "KubernetesServiceHatoba#KubernetesServiceHatoba",
            "package": "KubernetesServiceHatoba",
            "packageVersion": "1.0.0"
        }
    }
}
  • build.gradle.ktsのdependenciesにsmithy-openapiを追加します
build.gradle.kts
plugins {
    id("software.amazon.smithy").version("0.6.0")
}

configure<software.amazon.smithy.gradle.SmithyExtension> {}

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation("software.amazon.smithy:smithy-model:1.15.0")
    implementation("software.amazon.smithy:smithy-aws-traits:1.15.0")
    implementation("software.amazon.smithy.typescript:smithy-typescript-codegen:0.8.0")
    implementation("software.amazon.smithy:smithy-openapi:1.15.0")
}
  • gradle build コマンドを実行することでOpenAPIのJSONが生成されました!
  • JSONファイルをSwagger UIで表示させたり、Amazon API Gatewayに設定したりといったことが可能です
  • ただし異なるIDLなので全てをサポートしているわけではありません、詳しくはSmithy Guidesを参照してください

まとめ

  • Smithyを利用してAPIリファレンスのモデル定義とモデルからコード生成やOpenAPIへの変換についてを紹介しました
  • APIリファレンスを表計算ツールの方眼紙やwikiで記述している方も多いと思いますが、それだと記述レベルの統一や運用管理が難しいと思うので自動化の観点からもSmithyの利用を検討してみてください

おわりに

明日は@aokumaが「社内 GitLab の大型マイグレーションをした話」を書いてくれるそうなので楽しみにしましょう🐭

Discussion