KiotaでOpenAPIの定義からGoのクライアントを生成してみる

こんにちは。 kimihiro_nです。

Microsoftから「Kiota」というOpenAPIの定義からクライアントコードを生成するツールが公開されていたのでちょっと触ってみました。

learn.microsoft.com

Kiota の特徴

JSON、YAMLで書かれたOpenAPIのAPI仕様から、APIを呼び出すためのクライアント部分を自動生成してくれるツールです。 特徴としてはGoやPythonなど様々な言語への書き出しに対応していて、似たようなインターフェースで扱える点になります。 API仕様を一度共通の内部的なモデルに変換し、そこから各言語のクライアントを生成する面白いアプローチを取っています。

似たようなツールだとOpenAPI Generatorという有名なものがありますが、 ツール自体が巨大になってきてしまっているのと、CLIからだとGo言語でクライアントのみのコードを生成する方法が分からなかった(サーバー側のコードも一緒に生成されてしまう)ため、クライアントに特化したKiotaを試してみることにしました。

Kiotaを使ってみる

インストール

learn.microsoft.com

インストール手順はこちら。 コミュニティ作成によるものですがbrewによるインストールもできます。

OpenAPI の用意

クライアントを作るOpenAPI仕様を用意します。

swagger: "2.0"
basePath: /api/v1
definitions:
  main.ErrResponse:
    properties:
      detail:
        type: string
    type: object
  main.Post:
    properties:
      content:
        type: string
      id:
        type: integer
      title:
        type: string
    required:
    - content
    - title
    type: object
  main.PostListResponse:
    properties:
      posts:
        items:
          $ref: '#/definitions/main.Post'
        type: array
    type: object
info:
  contact: {}
  title: ExampleWebAPI
  version: "1.0"
paths:
  /posts:
    get:
      consumes:
      - application/json
      description: get posts
      parameters:
      - description: limit
        in: query
        name: limit
        type: integer
      - description: offset
        in: query
        name: offset
        type: integer
      produces:
      - application/json
      responses:
        "200":
          description: OK
          schema:
            $ref: '#/definitions/main.PostListResponse'
        "500":
          description: internal server error
          schema:
            $ref: '#/definitions/main.ErrResponse'
      summary: Get List of Posts
      tags:
      - posts
    post:
      consumes:
      - application/json
      description: add post
      parameters:
      - description: Post
        in: body
        name: post
        required: true
        schema:
          $ref: '#/definitions/main.Post'
      produces:
      - application/json
      responses:
        "201":
          description: created
          schema:
            $ref: '#/definitions/main.Post'
        "400":
          description: invalid params
          schema:
            $ref: '#/definitions/main.ErrResponse'
        "500":
          description: internal server error
          schema:
            $ref: '#/definitions/main.ErrResponse'
      summary: Create new post
      tags:
      - posts

投稿の一覧取得と投稿が出来る簡単なAPIです。 諸事情(後述)でOpenAPI 3.xではなくてOpenAPI 2.0の仕様に合わせて作っています。 2.0の方はSwaggerとも呼ばれてます。

Goクライアントの生成

こちらからKiotaのGoクライアントを生成してみます。

kiota generate -l go -d ./swagger.yaml -o ./client -n ${レポジトリ名}/client

-l オプションでGo言語を、-d オプションでOpenAPI(Swagger)ファイルの置き場を、-o で出力先ディレクトリを、 そして -n で生成されるクライアントのフルパッケージ名を指定します。 フルパッケージ名は生成されたコードのimportが正しく動くために必要で、go mod init したときのレポジトリ名 + 出力時のディレクトリ(/client)を指定します。

生成されたファイル

実行するとこのようなファイルが生成されます。 これらのファイルは触らなくて大丈夫ですがimportのエラーが出ている場合は-nオプションに渡している値が適切かどうかを確認してみてください。

Goクライアントを使ってみる

まずはAPIクライアントを初期化から。

package main

...
import "github.com/microsoft/kiota-abstractions-go/authentication"
import "github.com/microsoft/kiota-http-go"
import ${レポジトリ名}/client

func main()
    ctx := context.Background()
    authProvider := authentication.AnonymousAuthenticationProvider{}
    adapter, err := http.NewNetHttpRequestAdapter(&authProvider)
    if err != nil {
        log.Fatalf("Error creating request adapter: %v\n", err)
    }
        // サーバーのURLをセット
    adapter.SetBaseUrl("http://localhost:8080/api/v1")
    apiClient := client.NewApiClient(adapter)

API認証が必要なケースに対応するためのauthProviderを生成し、そこから通信用のadapterを作っています。 今回は認証不要のAPIを叩くのでAnonymousAuthenticationProviderを利用しました。 adapterはAPI通信部分を吸収する部分で、ここで独自のHTTP通信クライアントを組み込んだりも出来ます。 OpenAPIにサーバーのエンドポイントの記載がない場合はこのようにadapterで指定します。 adapterからクライアントを生成したら準備完了です。

limit := int32(3)
params := posts.PostsRequestBuilderGetQueryParameters{
    Limit: &limit,
}
// リクエストを送信
result, err := apiClient.Posts().Get(ctx, &posts.PostsRequestBuilderGetRequestConfiguration{
    QueryParameters: &params,
})

リクエストを組み立ててAPIを呼び出す部分のコードはこのような形です。 APIごとに専用の構造体が生成されているのでそれを利用して組み立てる形になります。 パラメータが未指定の場合と区別するためすべてポインタ型で渡すようなインターフェースになっています。 数値から直接ポインタを取ることはできないので、一度代入が必要な点は少し手間になります。 lo.ToPtr みたいな関数を利用してもいいかもしれません。

result, err := apiClient.Posts().Get(/* 省略 */)

if err != nil {
    // OpenAPIで定義されているエラーは errors.As でキャスト出来る
    var errResp *models.ErrResponse
    if errors.As(err, &errResp) {
        fmt.Printf("request error: %s\n", *errResp.GetDetail())
        return
    }
    fmt.Printf("Error getting inference result: %+v\n", err)
    return
}
// Post の一覧を出力
for _, post := range result.GetPosts() {
    fmt.Printf("id: %d, title: %s, content: %s\n", *post.GetId(), *post.GetTitle(), *post.GetContent())
}

/* 実行結果例
id: 1, title: First Post, content: This is first test post.
id: 2, title: Second Post, content: This is second test post.
id: 3, title: Third Post, content: This is third test post.
*/

リクエストを行うと、パースされたレスポンスとエラーにアクセスすることが出来ます。 エラーはOpenAPIに定義されていれば専用のモデルが生成されるので、キャストすることでエラーオブジェクトにもアクセスすることが可能です。 定義されていないエラーの場合は汎用的なエラーが返ってきます。 正常時のレスポンスもパースされた状態で入っておりGetter経由で好きに取り出すことが出来ます。 JSONのパースなどを自分で記述しなくていいのはコード生成の大きなメリットです。

// リクエストデータの作成
requestData := models.NewPost()
title := "Hello world"
requestData.SetTitle(&title)
content := "This is a test from kiota apiClient."

// リクエストに追加
requestData.SetContent(&content)

// リクエストを送信
result, err := apiClient.Posts().Post(ctx, requestData, &posts.PostsRequestBuilderPostRequestConfiguration{
    Headers: header,
    Options: []abstractions.RequestOption{
        kiotaHttp.NewCompressionOptions(false),  // リクエストBodyのgzipをオフに
    },
})
// 作成した Post の ID を出力
fmt.Printf("saved id: %d\n", *result.GetId())

/* 実行結果例
saved id: 4
*/

POSTを使って送信する例はこちら。 こちらも専用のモデルが定義されているのでSetterを利用して値をセットしていく形になります。

試してみたときのはまりどころとしては、デフォルトだとリクエストが圧縮されて送られてしまうことでした。 サーバー側でリクエストが弾かれてしまい、原因を探っていったところBodyをgzipして送信していることが分かりました。 リクエスト時のオプションとしてCompressionをfalseにセットしてあげることで Content-Encoding: gzip に未対応なサーバーでも適切にリクエストを送ることが出来ます。

OpenAPIの生成もコードファーストでやってみる

OpenAPI 3.xではなくOpenAPI 2.0の形式でYAMLを生成していた部分の答え合わせなのですが、今回OpenAPIの生成もGoのコードから作ってみることにしました。

Python だとFastAPIのようなフレームワークで簡単に出力できますが、Goだとフレームワークと一体でOpenAPIを生成してくれるものは見当たりませんでした。 OpenAPIを用意してサーバー側のコードを生成する「スキーマファースト」なやり方であれば、OpenAPI Generatorをはじめとして複数候補がありますが、 OpenAPIを手で書くのが辛い、ファイル分割の対応具合がツールによってまちまちなどの理由で「コードファースト」なやり方を模索してました。

github.com

最終的にたどり着いたのがSwagというツールでした。 こちらはAPIサーバーにコメントの形でOpenAPIに必要な情報を埋め込むことでOpenAPIのスキーマを生成してくれるツールです。 gin, echo, fiberなど主要なWebフレームワークにも対応しています。

type Post struct {
    ID      int    `json:"id" binding:"-"`
    Title   string `json:"title" binding:"required"`
    Content string `json:"content" binding:"required"`
}


// AddPost
// @Summary Create new post
// @Schemes
// @Description add post
// @Tags posts
// @Accept json
// @Produce json
// @Param post body Post true "Post"
// @Success 201 {object} Post "created"
// @Failure 400 {object} ErrResponse "invalid params"
// @Failure 500 {object} ErrResponse "internal server error"
// @Router /posts [post]
func (s *Server) AddPost(g *gin.Context) {
    post := Post{}
    if err := g.ShouldBindJSON(&post); err != nil {
        g.JSON(http.StatusBadRequest, ErrResponse{Detail: err.Error()})
        return
    }
    if err := s.postRepository.AddPost(g, &post); err != nil {
        g.JSON(http.StatusInternalServerError, ErrResponse{Detail: "internal server error"})
        return
    }
    g.JSON(http.StatusCreated, post)
}

このような形で各ハンドラにコメントを付与し、swagコマンドを叩く事でOpenAPIのスキーマを生成できます。 (上記はginの例になります。) Postなどの構造体名をコメントに入れておくとOpenAPIに反映してくれるのでスキーマの更新漏れがなくて便利です。

欠点としては生成されるOpenAPIが2.0相当になってしまう点です。 3.0が公開されたのが2017年なので、OpenAPIまわりのツール対応状況を考えると3.xに移行しておきたいところです。 v2という形でSwagのOpen API 3.x対応が進められていますが現時点ではリリースされていません。 RCまでは来ているようなのでそのうち公開されると思いますが。

まとめ

SwagとKiotaを利用することで、システム間の連携を扱いやすくすることが出来ました。 OpenAPIを利用してスキーマを管理しつつ、プログラムの上ではOpenAPIを意識せず扱えるので改修やメンテナンスも行いやすくなりそうです。

弊社の場合PythonとGoをよく利用するので、「PythonのFastAPIでOpenAPIを生成してKiotaのGoでクライアント生成」、「SwagでOpenAPIを生成してKiotaのPythonクライアントを生成」みたいな言語を跨いでの連携もしやすそうです。 似たようなシステム連携をするのに「Protocol Buffers」も選択肢に上がると思いますが、REST APIでの資産が既にある場合、現状のコードベースを生かしつつSwagなどでOpenAPI化し、自動生成されたクライアントで連携を堅固にしていくのも実用的かと思いました。

今回解説用に作成したサンプルコードはこちら github.com


The Go gopher was designed by Renée French.

APIクライアント「Insomnia」で始める、チーム開発効率化

JX通信社の CTO の小笠原(@yamitzky)です。本日は、最近社内で検証している API クライアントの「Insomnia」や、Insomnia を活用したチームでの API 開発の効率化についてご紹介します。

Insomnia とは

Insomnia は、オープンソースの API クライアントです。API 通信を GUI で直感的に検証・保存できる、というのが最も基本的な機能です。似たようなツールだと Postman などが有名だと思います。

insomnia.rest

Insomnia は一般的な REST API だけでなく、GraphQL や gRPC の API にも対応したツールです。JX通信社では、NewsDigest や FASTALERT などのサービスで GraphQL を活用しているため、GraphQL にネイティブ対応しているのは非常に便利です。

Insomniaのスクリーンショット。各リクエストを保存しておくことができる。GraphQLの対応や、jq的な絞り込みもできる

また、後述のように Insomnia にはチーム開発のための機能(コラボレーション機能)が備わっており、保存したリクエストのリストを他の人と共有することもできます。

Insomnia を導入するモチベーション

Insomnia の最も基本的な使い方は API クライアントとしての使い方です。これだけであれば、GraphQL Playground や、Swagger UI などでも事足りるかと思います。

今回、Insomnia を導入することで、次のような課題を解決できないかと考えました。

1. API 通信の様々なユースケースの記録

同じ通信エンドポイント(URL)に対して「◯◯でフィルターする場合」「◯◯でソートする場合」「不正なデータを入れた場合」といった複数のユースケース(利用想定)が紐づくことがありますが、これらを網羅的に保存できるようにしたいです。

2. スキーマ定義がされていない社内 API のドキュメント

一部、昔開発した API サーバーなどでは、GraphQL や OpenAPI でのスキーマ定義がされていないものが残ってます。これらの API の使い方のドキュメントについても、統一した場所に保存しておきたいです。

3. 成果物の共有

スプリントレビューのような成果報告会で営業側のメンバーに GUI で API を叩いてもらう場合や、普段 API の開発に入っていないエンジニアメンバーにさっと共有したい場合があります。

4. 認証方法が複雑なプロジェクトのデバッグ

Google Cloud の Cloud Run で開発した認証付き API だと、Bearer Token を都度 CLI で生成して設定する必要があります。このような認証が設定されていても、容易にデバッグできることが望ましいです。

これらの「保存」と「共有」の課題を解決するために、今回 Insomnia の導入を検討しました。特に、本稿で紹介するような基本機能だけであれば、Insomnia の共有機能は無料で使えるのが嬉しいポイントです(料金プラン)。

Insomnia の共有機能

Insomnia では、右上の「+Share」ボタンを押すことで、他のメンバーに共有することができます。

その際にデフォルトで設定されているのが E2EE(エンドツーエンド暗号化) です。利用者が管理する鍵を元に、保存したリクエスト内容は暗号化されます。逆に、パスワードを忘れるとアクセスできなくなってしまうため気をつけてください*1

共有されたプロジェクトは、 他のユーザーと共同編集ができます。好みが分かれるところですが、Notion や Google Docs のような同期的な共同編集ではなく、Git のような概念 (commit / push / pull) による共同編集がベースとなっています。プロジェクトの環境変数(例:API の URL) を共有することもできます。

有償プランでは、実際に Git のリポジトリと紐づけたりもできるようです。

その他の便利機能

OpenAPI

Insomnia は、OpenAPI のエディターとしての機能も備わっています。バリデーションしてくれたり、定義を元に Insomnia のリクエストを一括生成→ curl や通信プログラムとしてコピーできるようにしたりもできるので非常に便利です。

テスト

あまり検証していませんが、API 通信のテストケースも保存しておくことができます。システム障害発生時の初動調査やリリース前の一括動作チェックなどにも使えそうです。

変数埋め込み

Insomnia は環境変数の概念があり、これを DRY に埋め込むことができます。API のドメインや API キー等を設定し、本番/開発環境を切り替えるのにも便利です。環境変数の情報も他チームメンバーと共有することができます。また、逆に共有しない「Private enviromnent」を設定することもできます。

このような環境ごと設定値の埋め込みだけではなく、プロンプト(リクエスト実行時に発火するフォームダイアログ)の結果を埋め込む機能や、動的に生成した UUID やタイムスタンプ、OS 情報などを埋め込む「Template tag」という機能もあります。

外部プラグイン

実は、Template Tag の機能は、サードパーティーや独自に開発したプラグインによって、拡張することができます。これらのプラグインはPlugin Hubに公開されています。

例えば、Cloud Run の API の認証には cloud run auth を使えば自動的に認証情報を埋め込むことができます。プラグインのソースコードは公開されているので、参考にして自作もできますね。

まとめ

本稿では、Insomnia というツールと、検証している共有機能などの紹介をしました。無料で始めることができる、というのは、スタートアップ企業にとっては嬉しいのではないでしょうか?

他にみなさんが使っているツールあれば、ぜひコメントなどで教えていただけると嬉しいです。良いお年を!

*1:JX通信社ではパスワード管理ツールを使っています

ダウンタイムを抑えてAWSからGoogle Cloudにデータベースを移行したはなし

こんにちは。kimihiro_nです。

今回はプロダクトで使用しているデータベース(MySQL 互換)を AWS から Google Cloud に引っ越ししたときのはなしを紹介します。

AWSから Google Cloud へ

AWS では MySQL 5.7 互換の Aurora グローバルデータベースを利用していました。

グローバルデータベースを使っているのは、大規模災害時におけるリージョンレベルでの障害に備えるためのもので、万一リージョンレベルの障害が発生してもサービス継続できるような体制を作っていました。

今回ある事情から Google Cloud の CloudSQL へのお引っ越しを行い、同じようにホットスタンバイでのマルチリージョン構成を構築することになりました。

なぜ AWS から Google Cloud に

恐らく一番気になるのがこの理由の部分かもしれませんが、大人の事情ということで詳細は伏せさせてください。 大きな理由としてはコストなのですが、Google Cloud に移行した方が絶対的に安くなるというわけではないので、いろいろ総合的な判断をした結果ということになります。 データ基盤が BigQuery 上で構築されていて連携がしやすいみたいな分かりやすいメリットもあります。

ちなみに Aurora は2017年くらいから利用していますが、ここ数年データベース由来での障害が発生していないので非常に優秀なデータベースサービスだと思います。

AWS から Google Cloud に移行する上での調査

どちらも MySQL 互換とはいえ Aurora と Cloud SQL では仕組みに違いがあります。

違いについてはこちらの Google Cloud 公式のドキュメントに既にまとまっていたので、こちらをよく確認し問題がないかを検証しました。

ここでの一番の懸念点はパフォーマンスでした。Aurora は独自にチューニングされて高いスループットが出るようになっているため、移行した途端データベースが悲鳴をあげてしまうことが考えられました。とはいえパフォーマンスのうたい文句などから推測で判断することも難しいです。

なので実際 Cloud SQL に建てたデータベースを用意してLocustを用いた負荷試験を実施しました。シナリオ自体は以前にDBの負荷をみるために作ったものがあったので、今回はそれを流用し Aurora に繋いだときと Cloud SQL に繋いだときとでレスポンスタイムの比較を行いました。結構重めのクエリも入っていたのですが Aurora のときと大して遜色なく捌く事ができて一安心でした。

プライマリ↔レプリカ間の同期ラグについても検証してみましたがこちらも許容範囲内でした。

どうやって移行したか

移行可能ということが確かめられたので実際の移行手順について検討をしていきました。

移行する上での前提

  • 少ないダウンタイムでの移行

災害・事件・事故といったリスク情報を速く伝えるサービスなので、ユーザーが利用できなくなってしまう時間は極力減らしたいです。とはいえ完全に無停止でやろうとすると移行の工数と難易度が跳ね上がってしまうため、メンテナンスの事前アナウンスをしつつ短いダウンタイムで移行できるような方針を検討したいです。

  • 常時情報の流入がある

ダウンタイムに関連する話ですが、データベースには毎秒かなりの数の書き込み操作が行われています。そのため新旧データベースを動かしながら無停止で乗り換えるみたいな事が難しく、データの不整合を避ける意味でも短時間の書き込みの停止は許容するようにしました。ニュースなどの取り込みはDBに書き込む前にキューを活用していたので、切り替え中の時間帯のニュースも取りこぼすことなく取り込みが可能です。

  • プライマリ レプリカ構成(マスタースレーブ構成)

データベースは読み書きが可能なプライマリと読み取り専用のレプリカの複数台構成になっていて、サービスの主機能はほぼ読み取り専用のデータベースから取得しています。そのため、プライマリDBへの接続を一時停止してもエラーで何もできないといった状況は回避できるようになっていました。

  • DBのサイズ

データベースのサイズは数百GBほどあります。数TBとかそういった規模ではないのは幸いですが、それでもサッとコピーして終わる規模でもありません。

  • AWS VPC

Aurora のデータベースは VPC 内に配置してあり、データベースを利用するアプリケーション(ECS、 Lambdaなど)もVPC内に配置して接続出来るようにしています。今回アプリケーションの Google Cloud への移行は行わず、データベースの移行だけを行うことを目的としています。そのため Cloud SQL と AWS の VPC をどう接続するかも考える必要がありました。

移行方針

上記のような前提から、このような方針ですすめることにしました

  • Cloud SQL へレプリカを構築し、アプリケーションの読み込み系統をまず移行する
  • 書き込み系統の移行は事前に顧客周知を行い、メンテナンス時間を設けて移行する
  • AWS と Google Cloud は VPN を構築し相互に通信できるようにする

データベースの読み込み系統(レプリカへのアクセス)と書き込み系統(プライマリへのアクセス)の2段階にわけて移行する方針としました。

先にレプリケーションを利用して Cloud SQL の方にデータベースを用意しておき、Readerへアクセスしていたアプリケーションをすべて移行してしまいます。このときはレプリカからレプリカへの切り替えなので無停止で実行が可能です。

次にアプリケーションの書き込みも Cloud SQL へ向けるよう切り替えを行います。このときは不整合を防止するためサービスの書き込みを一時停止してアプリケーションの切り替え作業を行います。メンテナンス中も読み込み系統は生きているためサービスの閲覧などはそのまま利用が可能です。

この移行作業を行うにあたり、Database Migration Service(DMS) というサービスを活用することにしました。

Google Cloud の機能として用意されているマイグレーション用の仕組みで、既存の DB から簡単にレプリカを生成することができます。動きとしては手動で MySQL のレプリカを作成するのを自動化したものに近くて、「Cloud SQLの立ち上げ」「mysql dumpの取り込み」「dump以降のリアルタイムな差分取り込み」をやってくれます。今回は MySQL 互換同士でしたが、異なるデータベース種別間でも移行が可能になっているみたいです。

また、データベースのネットワークに関しては VPN を構築することにしました。 移行に際し VPC 内からデータベースアクセスを行う構成を崩したくなかったため、AWS と Google Cloud 間に VPN を構築して相互にやりとりできるようにしました。冗長な構成にもみえますが DB 移行後にアプリケーションも Google Cloud へ移行していくことを想定したときに、同じネットワークとして扱えるメリットが大きかったのでこのようにしています。

移行

VPN 構築

まず始めに Google Cloud ↔ AWS間のVPN構築するところから実施しました。

こちらの記事を参考に Terraform で VPN の HA 構成を取っています。

VPN 以外にもサブネットやルーティングの設定が必要で手こずりましたがなんとか相互で通信する事ができるようになりました。

DBのエンドポイントにドメインを割り当て

今回アプリケーションのデータベース切り替え作業にあたり、DNSを用いて切り替えが簡単にできるようセットアップを行いました。

プライマリ用とレプリカ用のホストにそれぞれ固有のドメインを割り当て、ドメインの向き先を変えることでアプリケーションの接続先も追従するようにしました。

こうすることで ECS、Lambda にあるアプリケーションを再デプロイしたりせずに切り替えができます。

また Cloud SQLでは Aurora のような接続エンドポイントが提供されず、インスタンスの IP を指定して接続する形なのでドメインを割り当てておくことでラウンドロビンをしたりもできるようになります。

Route 53 を使って読み込み系統用、書き込み系統用のドメインを作成し、CNAME で Aurora のエンドポイントを指定しました。

DNS で切り替える際の注意としては、アプリケーション側でデータベース接続を維持していると、DNS の向き先が切り替わった事を知らないまま古い DB に接続し続けてしまうことがあるので、一定時間で再接続するよう設定しておく必要があります。

Database Migration Service起動

DMS を利用して Google Cloud に レプリカ を立ち上げます。VPN 構築済みなので、あとは DB の接続情報を入れるくらいで自動でレプリカインスタンスを作ってくれます。

RDS からの移行の場合、mysql dump を開始するときに一時的にプライマリの書き込みを停止する必要があり、このとき作業込みで5分くらいの書き込みダウンタイムが発生します。

それ以外はほぼ全自動で、ダンプの取り込みが完了すれば読み込み専用データベースとして利用できるようになります。取り込みの速度はだいたい100GB/時間ほどでした。

読み込み系統切替え

Cloud SQL にレプリカができたので、読み込み系統をまず移行します。Route 53のドメインを切り替えるだけなのですが一つ大きな問題があります。

DMS では MySQL の接続ユーザーは同期されないため、そのまま接続しようとしても認証に失敗してしまいます。

Database Migration Service 実行中はデータベースも読み取り専用になっていて困っていたのですが、Cloud SQL のコンソールから接続ユーザーが作れる事が分かったので、そちらを利用して既存の接続情報を移植しました。なおコンソールから作成したユーザーは root 権限がついてしまうのであとで権限を修正するなどの作業が必要です。

書き込み系統切替え

DB 移行の大詰めである書き込み系統の移行です。DMS の管理画面で「プロモート」を実行するとソースのデータベース(ここではAWS)から切り離され、独立したプライマリの DB として利用できるようになります。このとき5分ほどのダウンタイムが発生します。

無事プライマリが起動したことを確認したらアプリケーションを Google Cloud 側に切り替えて移行が完了です。

レプリカ作成

最後に Google Cloud 側にレプリカを構築して AWS のときと同じような構成に戻します。読み込み系統切替えの時点で Google Cloud 側にもレプリカを用意しておきたかったのですが DMS の管理下にあるときはレプリカの作成などができないため、移行が完了してからレプリカを構築しています。

読み込みがヘビーな場合は一時的に強めのインスタンスにして移行する必要があるかも知れません。

また、図には記載していないですがリージョンレベルの障害に備えるため別リージョンに置いたレプリカも別途用意しています。Google Cloud の場合 VPC をリージョンをまたいで構築できるのでとても簡単でした。

おわりに

DMSを活用することで DB 移行の手間を大幅に減らすことができたように思います。気がかりだったサービスのダウンタイムも、ダンプ取得時の5分と書き込み系統移行時の5分くらいと短時間で完了することができました。

移行時の大きなシステムトラブルもなく一安心です。

データベースをそっくり移行する機会はなかなかないと思いますが参考になったら幸いです。

チーム規模が変動するチームでスクラムマスターとしてやってきたこと

お久しぶりです。シニアエンジニアの @sakebook です。今回私がスクラムマスター(以下SM)として所属するチームで、SMとして、およびチームで取り組んできたことなどを共有します。

背景

他社事例なども見聞きするなかで、今まで何をやってきたのかを共有することはそれなりに誰かの役に立つことがあると思ったのでこの度まとめました。

前提

この手の話を見聞きする上で前提が異なっていると解釈しにくいので、私たちのチームについて説明します。

  • チームには業務委託や副業の方、つまり週5稼動ではない人たちもいました。
  • 最大でエンジニアは10人強いました。出入りは数ヶ月単位で発生していて、最小は5人です。
  • PBI管理にはJIRAとNotionを併用しています。
  • 1スプリントは2週間です。
  • チームはフルリモートです。

リファインメントの整理

スプリント計画で時間を押してしまうことが多く、リファインメントが慢性的に足りていないようでした。

リファインメントは30分x4で週に2時間の枠だったのですが、30分x8の4時間の枠にしました。

スクラムガイドでは作業時間の10%以下にすることが多いとあったので、その最大まで枠を確保しました。

初めはチームからも「多いのでは?」という声もあったのですが、やってみると「今までむしろリファインメントが足りていなかった」と感じる声も上がって、今までよりスプリントゴールへの自信度があがりました。毎回実施するのはなく、不要な時はスキップしたり柔軟に対応しています。

詳細見積もりの分離

リファインメントで、詳細が曖昧な部分、または実現方法が複数あって決めきれない場合などに話しきれないことが度々発生していました。これはストーリーポイントを見積もるタイミングがリファインメントしかないとチームで受け止めていて、なるべく正確に見積もろうという意識から発生しているようでした。

そこで、詳細見積もりの時間を枠として設けました。目的は見積もったPBIおよび見積もるPBIのストーリーポイントの精度を上げることです。こちらもリファインメントと同じだけ確保していますが、リファインメントとの違いとして次の特徴があります。

  • POは参加しなくても良い
  • チームも必要な人のみで話す
  • チームが不要と思ったら予定を削除できる

詳細見積もりで調整ができる前提で、リファインメントでは詳細が曖昧な部分があったとしても、それによってストーリーポイントに変動が起きそうかどうかに着目してもらい以前に比べて気軽に見積もりができるようになりました。

また、結果的にスプリント計画でやっていた内容も分離でき、スプリント計画ではプランニングに集中できるようになりました。

今のチームではストーリーポイントの数字にはフィボナッチ数を採用しています。

ワーキングアグリーメントとストーリーポイントの基準の定期的な見直し

前提にある通り、チームの変動が度々ありました。チームへの受け入れドキュメントの一つとしてワーキングアグリーメント(以下WA)を活用しました。ストーリーポイントも、チームが異なれば基準が変わるので、どんなPBIだとストーリーポイントがどの程度になるのかを参照できるようにドキュメント化しました。それぞれクォーターごとに見直しイベントを設定し、更新しています。

別途スプリントレトロスペクティブでWAに採用したいことがあったり、見直したいという声があればクォーターを待たずに更新しています。

スプリントゴールに目的を添える

POから、スプリントゴールは目指してもらってるがその背景にある目的意識が浸透しているか不安という声がありました。

何のためにそのスプリントゴールを目指しているのかがぶれていると、チームで判断が必要になった時に自分たちで判断できなかったり、間違った優先度で進めてしまうことが起きてしまいます。

日々何のために開発しているのかを意識してもらうために、少し冗長にはなるのですがスプリントゴールに目的も含めるようにしました。このスプリントゴールを毎日リマインドし、デイリースクラムでも確認することで同じ目的を目指している意識を高めました。

この形式にするタイミングで、スプリントバックログにあるものを全部達成することが良いという考えから、スプリントゴールを達成するというタスクベースから目的ベースへと移行できました。

なので今のチームでは全AC(スプリントバックログの全消化)にはこだわっていません。

予期せぬ差し込み依頼などがあった時にも目的に沿って、後にすべきか先に対応しないといけないのかチームで判断できるようになってきています。

デイリースクラムの自分事化

デイリースクラムを朝会として毎日行っています。

SMのためにチームが日々の進捗確認をする場になっていました。

SMがいなくてもチームがやっていけるようにすることが必要なのと、目的がチーム内でスプリントゴール達成への障害に気づくことなので、朝会で確認するフォーマットを規定し、事前に自分たちで朝会前にフォーマットを埋めてもらうようにしました。

それまではSMがチームの話をメモしていくスタイルで話す粒度も人それぞれだった部分があったのですが、ブレが少なくなりより目的に対して機能する朝会になりました。

終わりに

多様なチームがあるのでそのまま適応できることもあれば適応できないこともあると思います。どういう課題があって取り組むと改善できるのかを見極めて小さく試していけると良いと思います。

もっと詳細を知りたいとかあれば話せる範囲で話せるので、Twitterでもコメントでも書いてもらえると私も嬉しいです。

APIをダウンタイムなしでAWSからGCPに引越しました

こんにちは。JX通信社 サーバーサイドエンジニアの内山です。

私が所属するNewsDigestチームでは先日、モバイルアプリ用APIをAWSからGCPにお引越しする、というプロジェクトを行いました。
会社の方針としてGCP利用を推進していることや、GCP特有のマネージドサービスの活用を視野に入れ、移行を決めました。

今回のAPI移行では、大きく以下2パートの作業がありました。

  1. APIのホスティングサービスをGCP上のものにするためのCI/CD設定, 定義作成, アプリケーション修正などの作業
  2. APIドメインのルーティング先をAWS -> GCPに切り替えるインフラ作業

本記事では、後者のルーティングに関して取り上げていきます。

作業概要・前提

今回は以下の制約を設けての移行としました。

  • APIのドメインを変更しない
  • ダウンタイムなしで行う

アプリ側には手を入れず、サーバーサイドチームで日中に完結させられるように・・と言う願いを込めた作戦です。

今回のメインテーマは、以下の点をケアすることです。

  1. 移行後の環境で最初からSSL証明書が有効な状態にする必要がある
  2. 何か問題が起こった場合に速やかに切り戻せる必要がある
  3. 移行を段階的に行なえると良い

これらについて、それぞれどのように対応したかを解説していきます。

有効なSSL証明書を事前に用意するには

選定理由などは省略しますが、今回のインフラ環境はCloud Load Balancing(以降CLB)を最前段に置いてバックエンドサービスにルーティングするという構成となっております。

CLBでは、HTTPSプロトコルのフロントエンドサービス定義を作成する際に、GCP環境に用意しておいた証明書を紐づけることで設定を行います。

なお、Cloud Load Balancingに紐付けられるSSL証明書は以下の二種類があります。

有効な証明書がない状態でリクエストを受けるとSSLエラーが発生してしまうので、GoogleマネージドSSL証明書の有効化のために稼働中のAPIのリクエストを向けるとなると、一時的なダウンタイムを許容するかメンテナンス中に行う必要があります。

一方、セルフマネージドSSL証明書は向き先変更前に有効な証明書を得ることができますが、証明書更新の手間が永続的に発生することとなります。

両者のいいところどりをしたい・・ダウンタイム許容したくないし運用もしたくない・・ということで、今回は以下の作戦を取ることにしました。

  1. セルフマネージドSSL証明書を紐付けた状態で向き先変更を行う
  2. その裏でGoogleマネージドSSL証明書を紐付けて有効化を行う
  3. GoogleマネージドSSL証明書が有効になったらセルフマネージド証明書をCLBから取り除く

結論、この手順で問題なく作業が行えました。GCPコンソールを利用する場合はほとんどぽちぽちで済むのですが、セルフマネージドSSL証明書の発行は手作業なため、再度行う際の備忘を兼ねて以下に手順を記載します。

セルフマネージドSSL証明書の発行・検証作業

セルフマネージドSSL証明書に関してはさまざまな発行・有効化の手法がありますが、今回はLet’s Encrypt証明書のTXT検証手順を採用しました。また、Let’s Encrypt証明書の発行手段として、今回はCertbotを利用することとしました。

詳細は省き、作成時の作業手順を紹介します。ドメイン名やメールアドレスは適宜書き換えて参考にしてください。

1: 以下コマンドで指定ドメインのTXTレコードに設定する文字列を取得

sudo certbot certonly --manual --domain api.example.com --email hoge@example.com --agree-tos --manual-public-ip-logging-ok --preferred-challenges dns

2: 該当ドメインのTXTレコードを作成

3: しばし待ち、手順1の画面でEnterを入力して検証実施

以上の手順で得られたfullchain.pemとprivkey.pemを、GCPのセルフマネージド証明書の要素としてコンソールなりAPIなりからアップロードし、しばし待てば準備完了です。

トラフィック移行前の確認

証明書が有効かを確認するためには最終的にはAレコードを変更することになりますが、ローカルで /etc/hosts を編集してドメインとCLBのIPを紐付ければローカル環境からは事前にHTTPSアクセスが有効か検証できます。

トラフィックを流す前にセルフマネージド証明書が有効化され浸透していることを確認することをおすすめします。

安全なトラフィック移行作業をどう行うか

さて、SSL証明書を事前に用意できたため、あとはドメインへのトラフィックをCloud Load Balancingに流してあげればOKです。
・・が、前述したようにドメインをそのままにすることと、段階的な移行や速やかな切り戻しを実現するため、このステップで必要となった工夫について記載していきます。

まずは、説明のため移行前のドメイン周りの状況を図示しておきます。

移行前は、Route53でホストしているドメインのCNAMEをCloudFrontに向け、そこからオリジンへ流すような構成を取っていました。

APIのドメインを変更しないことにしたため、上図で言うところのapi.example.comの向き先がGCP世界に向いている状態がゴールと言えます。

今回GCP上にデプロイしたAPIはCloud Load Balancingを最前段に置いています。CLBはAWSのALBやCloudFrontとは違ってドメインが振られず、ロードバランサーにIPを紐付けることで疎通させる仕組みとなっています。
そのため、CLBに紐付けたIPに向けたAレコードを作成することでドメインへのトラフィックをCLBに流すことになります。

ですが、利用中のドメインにはCNAMEがセットされているため追加でAレコードを設置できません。(ダウンタイムが許容できるなら、既存のCNAMEを外してバツっと差し替えても良いですが・・)

ではどうするのかというと、新ドメインでAレコードを作成して、そのドメインへのルーティングとして既存ドメインにCNAMEを作成します。

この構成を取ると、CNAMEでの加重ルーティングを用いることが出来るため、どのレコードに何%のトラフィックを流すか指定できます。

つまり、初期は10%程度GCPに流しておき、挙動に問題があればGCP側の加重を0にして戻す or 問題なければGCP側の加重を100にして全て流す、といったコントロールが可能です。

これで、課題としていた段階的な移行と速やかな切り戻しが実現できます。

まとめ

以上2パートの作業を組み合わせ、ダウンタイムなしで段階的にAPIのAWS->GCP切り替えを行うことができました。

実際の作業では、移行後のコストが嵩みすぎて一度AWSに戻したり、Cloud Load Balancingの後段のサービスを別のものに切り替えるイベントが発生したりと、何度か加重ルーティングに助けられたシーンがありました。

ワンチャンスで祈りながら移行・・ではなく、様子を見ながらゆっくり行える作戦で作業できたことが、精神的に本当に良かったですし運用工数も小さく絞れて大満足でした。

NewsDigestの根幹APIに関してはこれで移行完了となりましたが、JX通信社全体で見るとまだまだインフラでもアプリケーションでも大幅なリファクタリング・リアーキテクチャが望ましい箇所が残っています。

制約は諸々ありますが、その中で理想を追求し、プロダクトを自分の手で育てていきたい!といった志を持った仲間を募集していますので、ぜひご連絡ください!