vivit技術開発部におけるマイクロサービスへの考え方

こんにちは!vivitの技術開発部マネジャーの井島です。 開発部ではマイクロサービスアーキテクチャを採用しており、そのアーキテクチャの考え方について話したいと思います。

この分野は個人的に好きで、一生話してられます笑

はじめに

一般的な「マイクロサービス」の知識は知っている前提で、それをどれだけ自分たちの組織向けにアレンジできるかが、肝になると私は考えています。

マイクロサービスは会社の組織構造に密接に関係します。 ビジネス側の組織構造、開発側の組織構造、プロダクト、事業方針、技術方針、採用方針、(経営方針)など。 会社として最終的にアウトプットを最大化していかなければならない状況下で、どれだけ開発効率を出せる、会社の成長に追従できるシステムアーキテクチャにできるかどうか、という視点からの話になります。

技術的な観点や教科書通りのみで導入してしまうと、事業規模に対してコスト面や事業成長にネガティブに働いてしまうことも考えられ、全体的に考えることが必要です。 マイクロサービスも導入して終わりではなく、状況に応じて戦略的に成長させていくものだと考えます。

あなただったら、どのようにシステムのアーキテクチャをデザインしていきますか?

vivitの組織について

色々部署はありますが、エンジニアは技術開発部に所属しているかたちになります。

もう少し細分化して、技術開発部とビジネス側との関係性は以下になります。

ビジネス側は事業(プロダクト)毎に部署が別れています。 それに紐づくかたちで開発部内でもチーム分割され、ビジネス側とそれぞれが密に連携しています。

使用言語, フレームワーク

エンジニア構成

  • フロントエンドエンジニア
  • バックエンドエンジニア

開発部内各チームにはフロントとバックのエンジニアが所属します。

ほか全体的な状況

  • 開発部内各チームは、基本2人で構成される。 1チームのエンジニア人数が少ない。※開発計画によって人数は増減する。
  • 事業計画によって、プロダクトにかける開発工数が上下し、兼任もあり得る。
  • 開発内容は、ビジネス側の事業部毎に管理され開発に降りてくる流れ
  • 基本的にフロントエンド、バックエンドとで職務が別れている

マイクロサービスの群雄割拠

実は、vivitにマイクロサービスが導入されたときは、2019年あたりのレンタル事業、スポット事業が立ち上がったころでした。 この頃はvivitが新規事業を立ち上げていったころで、0→1の開発で開発部も1チームでした。 この頃のシステム構成は1チームが10前後マイクロサービスを開発するかたちでBazelも使って同時ビルドしていました。

マイクロサービス分割設計が境界付けられたコンテキストになっておらず、要件を満たすために結局複数マイクロサービスを開発することになり、開発は非効率なままでした。 その問題から、マイクロサービスの統一化が進みました。

たしかに、0→1 の段階でプロダクトの業務や機能も変化が激しい時期に業務単位でマイクロサービス分割するのはかなり難しいことだということが得られました。

マイクロサービスの考え方の1つに「作りやすい壊しやすい」がありますが、新規マイクロサービス作成にコストがかかる状況も、統一化の方向性に向けていたと思います。

マイクロサービス所有者

教科書的には1チーム1マイクロサービスを聞いたことがあると思いますが、vivit開発部の場合はどうでしょう。

1チームにフロントエンドエンジニアが開発するサーバ(Next.js)、バックエンドエンジニアが開発するサーバ(Go) がすくなくとも存在します。

野良マイクロサービス(明確な所有者不在)

プロダクト共通で使っているシステムも存在しています。「会員管理システム」と読んでいるもので、顧客情報/アカウントを管理し各プロダクトからログイン廻り共通で使われる部分です。 明確にビジネス側と紐づくチームは現在ありません。

放置されたマイクロサービス

マイクロサービス群雄割拠時代に取り残され、メンテナンスがほぼ入っていないマイクロサービスも存在しています。 プロダクト共通で使われている機能となっており、プロダクト都合の変更を入れづらい状況となっています。

一般的に1マイクロサービス内に複数ドメインの都合が染み出していくことはアンチパターンで、vivitでもこの考え方に従っています。 障害ドメイン分割、リリースの独立性の面もありますが、何より所有者不在かつ継続的に開発が入らないマイクロサービスに対してこのようなことを行うことは、 各プロダクトの開発要件を常に共通システムとして設計する必要があり、現状のvivit開発部でもアンチパターンとしています。

まとめ

設計する上での前提条件や、運用経験を並べてみましたが、マイクロサービスをどう適用しようか少しでもイメージ付きましたか?

vivit でのマイクロサービス構成は、基本的なマイクロサービスの考え方にプラスし、特徴は以下になっています。

  • 1チーム複数マイクロサービスを所有
  • ソースのレポジトリはチーム毎にモノレポ
  • マイクロサービス間でも技術的統一性を保つ
  • 新規マイクロサービス作成は慎重傾向
    • 基本はプロダクト毎、フロントエンド/バックエンド毎(開発が企画される部署単位)
  • 常にメンテナンス(開発)されるマイクロサービスとしていく
  • 放置マイクロサービスは引き続き放置
    • 機能追加などしたい場合はその機能をプロダクト所有マイクロサービスに移行することを検討
  • 会員管理のマイクロサービスのように所有者不在も一部許容
    • 必要に応じて各プロダクトチームの担当者が開発を行う
  • プロダクト毎のBFF
  • Kubernetes (GKE)
  • gRPC による同期的通信
  • バッチ処理 (CronJob)
  • [構想段階] ログ出力などマイクロサービス共通で使うもののライブラリ化
    • マイクロサービス毎の品質均一化
    • 開発効率アップ
    • 放置マイクロサービスを否とし、常にメンテナンスされる状況を是としており、共用だがエンジニアの目につくところ配置でき、この方式はvivitとしてアリ

コンウェイか逆コンウェイかと言われたら、コンウェイの法則に従っていると思います。 vivit の場合、複数事業があり1チームのエンジニアが最小限なことや、継続的なプロダクト開発をしつつ、開発リソースを集中させたプロジェクト的な開発も行われるところも、システムアーキテクチャ設計を難しくしています。しかし、それが面白いところでもあります。暇しないですね笑

少しでも気になったら、話を聞きに来てください!
いつでも歓迎します!

新卒採用

www.wantedly.com

キャリア採用

www.wantedly.com

www.wantedly.com

www.wantedly.com

Apollo で複数の GraphQL エンドポイントに接続する(+ Shopify Storefront API)

はじめに

フロントエンドエンジニアの関(@kur0buchi)です。

今回は自分の担当しているhinataレンタルで Shopify アプリケーションと連携する必要ができ Storefront API を使う事になりました。

hinata-rental.me

hinataレンタルを始めとした弊社各サービスでは元々バックエンドとの通信に GraphQL を使っており、本記事の趣旨としては新しく Storefront API の GraphQL エンドポイントに接続するために調査した結果となります。

TR;DL

ApolloLink.split メソッドを使うと、もう一つ GraphQL エンドポイントを使うことができる

www.apollographql.com

前提

GraphQL クライアントは Apollo となります。 また弊社フロントエンド環境(Next.js + TypeScript)での実装例になりますので、適宜読み替えてください。

要件としてシステム内のごく一部での連携のためこの方法を採用しておりますが、「普段から複数エンドポイントを使い分けている」「3つ以上のエンドポイントと連携する必要がある」場合には適用に際して再考の余地があるかと思われますのでご了承ください。

また Shopify Storefront API については必要最低限触れるのみで詳細な説明は省きます。参考とさせていただいたリンク先のブログ等をご参照ください。

okalog.info

準備

Shopify Storefront APIトークンを発行する

必要なアクセススコープ(権限)の設定を忘れずに行いましょう

動作確認

X-Shopify-Storefront-Access-Token ヘッダーに発行したアクセストークンを付与し、クエリを実行します。

権限に応じて欲しいデータが取得できていること、権限外のデータを取得する際はエラーになることが確認できます。

自分の作業環境では Altair GraphQL Client を愛用しています。

実装

Apollo クライアントを初期化する際に主要エンドポイントと連携用エンドポイントを定義し、 ApolloLink.split の第一引数に条件を指定します。

+  const primaryLink = new HttpLink({
+    uri: `${PRIMARY_API_URL}`,
+  });

+  const secondaryLink = new HttpLink({
+    uri: `${SECONDARY_API_URL}`,
+    headers: {
+      "X-Shopify-Storefront-Access-Token": SHOPIFY_STOREFRONT_TOKEN,
+    },
+  });

  const client = new ApolloClient({
-    link: new HttpLink({
-      uri: `${API_URL}`,
-    });
+    link: ApolloLink.split(
+      (operation) => operation.getContext().clientName === "secondary",
+      secondaryLink,
+      primaryLink
+    ),
  });

呼び出す際には初期化時に指定した clientNamecontext オプションに指定します。 いつものエンドポイントを使う際にはコードに変更が起こらない点も良いですね。

  • primaryLink
  const { data, loading, error } = useXxxQuery();
  • secondaryLink
  const { data, loading, error } = useXxxQuery({ context: { clientName: "secondary" } });

上記サンプルコードは graphql-codegen の生成物を想定しています、適宜読み替えてください。

終わりに

今回は一部分だけの適用を想定して比較的変更量の少ない実装で対応をするつもりですが、この調査を通じて ApolloLink 周りのまだまだ知らないオプションがあったりしたので深堀りしていけたらと思います。

vivit株式会社では先端技術を深堀りしていける仲間を募集しています。

新卒採用

www.wantedly.com

キャリア採用

www.wantedly.com

www.wantedly.com

www.wantedly.com

参考

受託開発の会社からvivitに入社して感じた4つのこと

こんにちは!今年12月からレンタルチーム(hinata rental)のバックエンドエンジニアとして入社した河原田です。

私はvivitに入社する前は受託開発会社のバックエンドエンジニアとして3年弱Golangを書いていました。

自社サービスを開発するのは初めての経験だったのですが、vivitに入社して感じた4つのことを紹介していこうと思います。

すごい数のマイクロサービスが動いている

vivitではキャンプに関する様々なサービスを展開しています。

各サービスのフロント・バッククエンド、それらを繋ぐBFF、ドメイン知識の関係ない機能など多数のマイクロサービスが存在します。

私が担当しているサービスは、レンタルサービスのバックエンドですが、これだけ多数のマイクロサービスが連携しているプラットフォームの開発経験はなく、とても新鮮な気持ちでいます。

体験しているメリットとしては、デプロイが容易であったり、1つのマイクロサービスをほぼ1人で担当することになるので、担当できる業務範囲が広いところでしょうか。

また一方でマイクロサービスが多いことで、管理の手間やサービスのオーナーが曖昧なものができたりと、縮小した方が良いのでは?といった議論なども行われています。

そんな経験も多くのマイクロサービスを動かしている環境ならではの話なので自分にとってはこれまた新鮮な体験です。

それぞれのサービス間の通信はgRPC+protobuf、GraphQLを使っており、スキーマ定義がきちんと書かれているので、定義を見ればどんな通信がされるのかは一目瞭然。

余分な説明も必要なく、開発にもスッと入ることができました。

vivit.hatenablog.com

ドキュメントでの業務知識・ナレッジが豊富

vivitでは基本的に各プロダクトにフロント・バックエンドのエンジニアがそれぞれ1人というチーム構成です。

そのため参画後、引き継ぎを行ったとしても細かい業務知識や実装ロジックをキャッチアップしないければいけない場面もあります。

受託開発を行っていたときは他社からの引継ぎ・ドキュメントが不十分で、何が行われているか、何が正かを理解するためにネストが深いコードを読んで、、、なんてことも多々ありました。

しかし、vivitでは全社でDocBaseというツールでドキュメント管理されています。 開発の経緯や要件すり合わせの記録、必要な業務知識など、困った時はとりあえずDocBaseで検索をかけると、必要な知識やとっかかりが得られるので、自分で考えるにしろ、人に聞くにしろ、仕事を進めるとっかかりを得やすい環境が整っていると思います。

最近だとドメイン駆動設計について理解が必要な場面があったのですが、数年前に行われた輪読会の内容まで細かに残っており、理解の手助けになりました!

マイクロサービス間でナレッジを盗める

vivitのバックエンドは主にGolangで書かれています。

そのため各サービスのバックエンド同士でコードレビューする機会があったり、リポジトリも自由に見ることができるので、他のサービスがどうやって書かれているのか気軽に参考にできます。

ちょっと前のことですが、レンタルの実装でDataLoaderを利用する機会があったのですが、お隣サービスのスポットのバックエンドをがっつり盗み見て、実装したことを覚えています。笑

vivit.hatenablog.com

心理的安全性が高い

 最後になりますが、vivitに入社してすごく感じることが心理的安全性がとても高くいなと感じています。特に自分が思っていることを発言しやすい。

いろいろな要因があると思うとのですが、私がそう思えるのはvivitとして心理的安全性を作ろうとする取り組みを、日々感じられるからだと思っています。

  例えば、開発部内では週1回、各事業の振替りを行うのですが、その目的の一つとして「 開発全員顔をあわせる場の創出」「より多くの人がこの場で話すことによる、部員間の心理的障壁の解消」ということが、しっかりと書かれています。

またskackチャネルでは個人のtimesチャンネルで自由に発言できる環境があったり、1on1に力を入れていたりと「個人が安心して働ける環境を作ろうとしてくれている!」と感じる機会が多いです。

vivit.hatenablog.com

実際に過去のアンケートでも、心理的安全性が高い!という意見が多く、納得の結果だなと思っています。

vivit.hatenablog.com

さいごに

いかがだったでしょうか?

vivitに興味を持ってもらった方に、入社したての感じたことを知ってもらおうと思いこの記事を書きました。

vivitではより多くの人が「もっと外が好きになる」ために、キャンプの楽しさを広げるお手伝いをしています。

キャンプが今好きな人も、これからキャンプを初めたい人も、一緒に働いてくれる仲間を募集中です。

気になったことがあれば、是非、お気軽に話を聞きに来てください!

【新卒採用】 www.wantedly.com

【中途: バックエンドエンジニア】 www.wantedly.com

【中途: フロントエンドエンジニア】 www.wantedly.com

gqlgenを使ったファイルアップロード機能を実装して感じたこと

キャンプ場を検索・予約できるサービス(以下hinata スポット)などの開発を担当している名嘉眞です。

hinata スポットなどvivitのいくつかのプロダクトでは、フロントエンドとバックエンドの間にBFFが存在しており、BFFではGraphQLを採用しております。 またBFFはGoで書かれていて、gqlgenというライブラリを使用しています。

今回は画像ファイルのアップロード機能をgqlgenを使って実装した話をします。

実装内容としては、gqlgenのドキュメントに記載されているファイルアップロードの例とほぼ同じです。 gqlgen File Upload このドキュメントを見てもらえれば実装はできるのですが、実装に際して感じたことを書いていこうと思います。

gqlgenのドキュメントを読む前

今回ブログとして書くきっかけになったタスクに着手する以前から、他のマイクロサービスで既に画像アップロード機能はありました。 実装内容としては、画像アップロードのmutationのパラメータに画像ファイルをbase64エンコードして含めるという方法でした。 この方法だと以下のような点で使いにくさがありました。

base64エンコードでアップロードする方法の使いにくい点

  • ファイル名はファイルとは別で渡す必要がある

  • string型で受け取るので扱いにくい(例:GCSなどにアップロードする際にio.Readerに変換する必要がある)

  • テストコードを書く際もファイルをbase64エンコードした文字列が必要

gqlgenのFile Uploadのドキュメントを読む

gqlgenのドキュメントに以下のように記載されています。

Graphql server has an already built-in Upload scalar to upload files using a multipart request. It implements the following spec https://github.com/jaydenseric/graphql-multipart-request-spec, that defines an interoperable multipart form field structure for GraphQL requests, used by various file upload client implementations. To use it you need to add the Upload scalar in your schema, and it will automatically add the marshalling behaviour to Go types.

GraphQLの様々なクライアントで運用可能なマルチパートフォームの仕様に準じて実装していて、使用するためには Upload スカラーの追加が必要とのことです。

仕様として公開されている方法でファイルアップロードの機能を実装できることはコードの保守性などを考えても良いことだと感じます。

以下のように Upload スカラーの追加することで、Upload型が定義できるようになります。

scalar Upload

type Mutation {
  uploadImage(input: UploadImageInput!): UploadImageOutput!
}

input UploadPlanImageInput {
  """
  画像データ
  """
  file: Upload!
}

生成されたGoのコードについて

gqlgenを利用してGraphQLスキーマからGoのコードを生成した結果が以下のようになります。

type MutationResolver interface {
    UploadImage(ctx context.Context, input model.UploadImageInput) (*model.UploadImageOutput, error)
}

type UploadImageInput struct {
    // 画像データ
    File graphql.Upload `json:"file"`
}

type UploadImageInput のFileフィールドは、graphql.Upload となっています。 この graphql.Upload は以下のような構造体です。

type Upload struct {
    File        io.Reader
    Filename    string
    Size        int64
    ContentType string
}

ファイル自体はio.Raader型で定義されていて、ファイル名やファイルサイズ、ファイルの種類も定義されていて使いやすそうです。

ここでこの記事の冒頭に記載したbase64エンコードでアップロードする方法の使いにくい点を振り返ってみます。

base64エンコードでアップロードする方法の使いにくい点

  • ファイル名はファイルとは別で渡す必要がある

  • string型で受け取るので扱いにくい(例:GCSなどにアップロードする際にio.Readerに変換する必要がある)

  • テストコードを書く際もファイルをbase64エンコードした文字列が必要

上記の使いにくい点がUpload型で改善できます。

  • ファイル名が構造体のフィールドとして定義されている

  • ファイルがio.Reader型なので扱いやすい。

  • io.Readerはinterfaceなのでテストコードも書きやすい(io.Readerを実装した別のオブジェクトに差し替えることができる)

使いにくい点が全て解決できました。もちろん全てユースケースにあてはまるわけではないと思いますが、基本的にはgqlgenを使う場合のファイルアップロードはUpload型を使っていきたいと思いました。

さいごに

vivit では、エンジニアを積極採用中です!

ポジションも様々用意しておりますので、GoやGraphQLを使用したプロダクト開発に興味を持って頂いた方は、是非カジュアル面談にお越しください。

新卒採用

www.wantedly.com

キャリア採用

www.wantedly.com

www.wantedly.com

www.wantedly.com

皆様とお話できるのを楽しみにしております!!

Cypress を使った E2E テストを GitHub Actions で実行する

こんにちは!!

vivit で SRE をやっている 宮本 です。

今回は hinata レンタル で行っている E2E テストを GitHub Actions で実行できるように CI パイプラインを構築したので、構成やポイントなどを紹介したいと思います!!

要件とか

  • コミットを GitHub に push したタイミングで E2E テストを実行する
  • E2E テストなので、依存する複数のマイクロサービスも立ち上げる必要がある
  • マイクロサービスのソースコードは CI を実行するリポジトリとは別のリポジトリに存在するものもある
  • 多少実行時間がかかったとしても、なるべくマイクロサービスをモックにすることなく、本番に近い環境でテストすることを優先する
  • vivit は開発環境、本番環境共に k8s を使っているので、CI上も k8s で環境を作れると嬉しい
  • E2E テストのプロセス自体は k8s 上ではなく CI 内で直に実行する
  • バックエンドマイクロサービスが使用するテストデータは簡単に変更できるようにする

vivit の技術スタックについては 、

vivit.hatenablog.com

をご参照ください 🙏

実際の構成

全体の構成を説明するのに不要な部分は削除したり、適宜内容を変えています。
そのままコピペしても動きませんので、ご了承ください。

ざっと以下のような構成となります!

name: E2E test

on:
  push:
    branches:
      - master
  pull_request:
    paths:
      - "aaa/**"

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - id: auth
        uses: google-github-actions/auth@v0.8.0
        with:
          credentials_json: secret

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v0.6.0
        with:
          project_id: secret

      - run: gcloud --quiet auth configure-docker

      - uses: actions/checkout@v3
        name: clone aaa
        with:
          repository: aaa
          ref: master
          path: aaa
          fetch-depth: 3
          token: token

      - name: pull aaa image
        working-directory: aaa
        run: |-
          git checkout master
          tags=($(git rev-parse HEAD | cut -c 1-7) $(git rev-parse HEAD~1 | cut -c 1-7) $(git rev-parse HEAD~2 | cut -c 1-7))
          for tag in "${tags[@]}"; do
            json=$(gcloud container images list-tags asia.gcr.io/secret/aaa --format=json --filter="tags:${tag}")
            if [ "$(echo "$json" | jq '. | length')" -eq 0 ]; then
              echo "docker image(asia.gcr.io/secret/aaa:${tag}) doesn't exists."
            else
              docker pull asia.gcr.io/secret/aaa:"${tag}"
              break
            fi
          done
          image_id=$(docker images --format "{{.Repository}}:{{.Tag}}:{{.ID}}" | grep asia.gcr.io/secret/aaa | cut -f 3 -d ":")
          docker tag $image_id aaa:test

        # 他にも同じ方法で複数マイクロサービスの docker イメージを pull してくる

      - name: start minikube
        id: minikube
        uses: medyagh/setup-minikube@master

      - name: docker build bbb
        uses: docker/build-push-action@v3
        with:
          context: .
          push: false
          load: true
          tags: bbb:test
          file: bbb/Dockerfile
          cache-from: type=local,src=/tmp/.buildx-cache-bbb
          cache-to: type=local,dest=/tmp/.buildx-cache-bbb-new,mode=max

      - name: minikube image load
        run: |
          minikube image load aaa:test
          minikube image load bbb:test

      - name: apply manifest
        run: |
          kubectl apply -k e2e/kubernetes
          sleep 3
          kubectl get pods -o wide

      - name: minikube service mysql
        id: minikube-service-mysql
        run: |
          mysql_address=$(minikube service mysql --url)
          host=$(echo $mysql_address | sed "s/http:\/\///g")
          arr=(`echo $host | tr ':' ' '`)
          echo "::set-output name=mysql_host::${arr[0]}"
          echo "::set-output name=mysql_port::${arr[1]}"

      - name: minikube service bbb
        id: minikube-service-bbb
        run: |
          bbb_url=$(minikube service bbb --url)
          echo "::set-output name=BBB_URL::$bbb_url"

      - name: Use Node.js 13.14.0
        uses: actions/setup-node@v1
        with:
          node-version: 13.14.0

      - name: yarn install
        working-directory: frontend
        run: yarn

      - name: insert test data
        run: |
          mysql \
          -h ${{ steps.minikube-service-mysql.outputs.mysql_host }} \
          -P ${{ steps.minikube-service-mysql.outputs.mysql_port }} \
          -uuser -ppass < e2e/data/bbb.sql

      - name: Run E2E Test
        working-directory: frontend
        env:
          API_URL: ${{ steps.minikube-service-bbb.outputs.BBB_URL }}
        run: |
          yarn build
          yarn test:ci

順番に説明していきます。

1. docker, gcp などが使えるようにセットアップ

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - id: auth
        uses: google-github-actions/auth@v0.8.0
        with:
          credentials_json: secret

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v0.6.0
        with:
          project_id: secret

      - run: gcloud --quiet auth configure-docker

ここは特に言うことありません。
後々、GCP のイメージレジストリである gcr から docker イメージを pull してくる必要があります。
なので GCP プロジェクトに対して認証を行っています。

2. 必要なマイクロサービスのリポジトリを clone してくる

      - uses: actions/checkout@v3
        name: clone aaa
        with:
          repository: aaa
          ref: master
          path: aaa
          fetch-depth: 3
          token: token

依存するマイクロサービスが複数あります。
それらのマイクロサービスが存在する vivit organization の他リポジトリを fetch-deps 3 で clone してきます。

vivit の場合はコミット SHA をタグに使って docker イメージを継続的にビルドしています。
そのため、最新の docker イメージタグを取得するためにリポジトリを clone しています。

3. 必要なマイクロサービスのイメージを gcr から pull してくる

      - name: pull aaa image
        working-directory: aaa
        run: |-
          git checkout master
          tags=($(git rev-parse HEAD | cut -c 1-7) $(git rev-parse HEAD~1 | cut -c 1-7) $(git rev-parse HEAD~2 | cut -c 1-7))
          for tag in "${tags[@]}"; do
            json=$(gcloud container images list-tags asia.gcr.io/secret/aaa --format=json --filter="tags:${tag}")
            if [ "$(echo "$json" | jq '. | length')" -eq 0 ]; then
              echo "docker image(asia.gcr.io/secret/aaa:${tag}) doesn't exists."
            else
              docker pull asia.gcr.io/secret/aaa:"${tag}"
              break
            fi
          done
          image_id=$(docker images --format "{{.Repository}}:{{.Tag}}:{{.ID}}" | grep asia.gcr.io/secret/aaa | cut -f 3 -d ":")
          docker tag $image_id aaa:test

step2 でテストに使用するイメージタグが取得できたので、あとはイメージレジストリから docker pull してくるだけです。

この時、イメージのビルド中であるなどの理由で、gcr 上にイメージが存在せず pull に失敗する可能性があります。
それを防ぐために、力技ですが最新のイメージが存在しない場合には次に新しいイメージを取ってくるようにしています。

pull が完了したらタグをテスト用に書き換えて終了です。

これをテスト実行に必要なマイクロサービスの数だけやっていきます!

4. k8s クラスターを立ち上げる

      - name: start minikube
        id: minikube
        uses: medyagh/setup-minikube@master

いよいよ、CI 内で k8s クラスターを立ち上げます 🎉
今回の場合、シングルノード構成で問題無いので、お手軽にクラスターを構築する為に minikube を利用してみました!

minikube.sigs.k8s.io

公式で紹介されているaction を利用するだけで簡単に構築できました。

5. CI を実行するリポジトリに存在するマイクロサービスをビルドする

      - name: docker build bbb
        uses: docker/build-push-action@v3
        with:
          context: .
          push: false
          load: true
          tags: bbb:test
          file: bbb/Dockerfile
          cache-from: type=local,src=/tmp/.buildx-cache-bbb
          cache-to: type=local,dest=/tmp/.buildx-cache-bbb-new,mode=max

リポジトリで管理されているマイクロサービスに関しては gcr に存在するイメージを pull してきました。

しかし本リポジトリに存在するマイクロサービスに関しては当然、CI 実行時のソースコードでテストを行いたいですよね。
その為、docker イメージの build を行い、後のテストで使えるようにします。

6. 用意した Docker イメージを minikube 上で使えるようにする

      - name: minikube image load
        run: |
          minikube image load aaa:test
          minikube image load bbb:test

ここまでのステップで必要な Docker イメージは CI が動いている環境に用意できました。
しかし minikube は CI の上に VM を立て、その中で k8s クラスターを起動しています。

よってそのままでは CI 上にあるイメージを minikube 側で認識することができません。
minikube image load コマンドを利用して minikube 側でイメージが利用できるようにします。

7. k8s マニフェストを minikube に apply する

      - name: apply manifest
        run: |
          kubectl apply -k e2e/kubernetes
          sleep 3
          kubectl get pods -o wide

いよいよマニフェストファイルを apply してマイクロサービスを全て立ち上げます 😆

これまで用意してきたマイクロサービスのコンテナに加えて、mysql が必要なので起動します。

8. minikube service で E2E テストプロセスから k8s svc にアクセスできるようにする

      - name: minikube service mysql
        id: minikube-service-mysql
        run: |
          mysql_address=$(minikube service mysql --url)
          host=$(echo $mysql_address | sed "s/http:\/\///g")
          arr=(`echo $host | tr ':' ' '`)
          echo "::set-output name=mysql_host::${arr[0]}"
          echo "::set-output name=mysql_port::${arr[1]}"

      - name: minikube service bbb
        id: minikube-service-bbb
        run: |
          bbb_url=$(minikube service bbb --url)
          echo "::set-output name=BBB_URL::$bbb_url"

7 でも説明した通り、 minikube は VM 上で立ち上がるので普通に localhost:{ポート番号}API のエンドポイントとして E2E テストを実行しても疎通できません。。。

minikube service コマンドを使って k8s 内の Service(L4)との通信経路を確立します!
後のステップで使えるように output の処理も行います。

9. mysql にテストデータを入れる

      - name: insert test data
        run: |
          mysql \
          -h ${{ steps.minikube-service-mysql.outputs.mysql_host }} \
          -P ${{ steps.minikube-service-mysql.outputs.mysql_port }} \
          -uuser -ppass < e2e/data/bbb.sql

8 で取得した mysql のアドレスに対してテストデータを INSERT します。

テストデータ自体はシンプルな SQL が書かれたファイルです。
このファイルを変更するだけでテストデータの調整ができるようになっており、アプリケーションエンジニアが頻繁に CI を触る必要が無いようにしています。

10. E2E テストの実行!!

      - name: Run E2E Test
        working-directory: frontend
        env:
          API_URL: ${{ steps.minikube-service-bbb.outputs.BBB_URL }}
        run: |
          yarn build
          yarn test:ci

いよいよ E2E テストを実行します!

その際の API エンドポイントは先ほど minikube service で取得したものです。
環境変数経由で読み込ませることで、 これまでに構築した環境に向けて実行できます。

実行時間

テストの実行までにこれだけやっていると気になるのが実行時間ですよね 🤔
今のところ、トータルで12分程度となっています。

単体テストに比べると明らかに長いですが、ギリギリ許容範囲でしょうか?!
テスト実行の次に時間かかっているのが、 minikube の起動で1分半ほどかかっています。

毎回 k8s をダウンロードしているので、ここをキャッシュ等で早くできると10分程度にできるかもしれません 🙂

E2E テストの場合、実行時間や基盤構築などのコストが上がるのは仕方ないと受け入れているのですが、もっと良い方法があれば知りたいなと思っているところです。

さいごに

長くなってしまいましたが、今回はマイクロサービス構成の E2E テストを Github Actions で実行する方法について解説してみました。

vivit のエンジニアは今回紹介したような新しい取り組みを積極的に行っており、一緒にエンジニアリングできるメンバーを大募集しています!

少しでも興味を持って頂いた方は、是非カジュアル面談にお越しください。

新卒採用

www.wantedly.com

キャリア採用

www.wantedly.com

www.wantedly.com

www.wantedly.com

皆様とお話できるのを楽しみにしております!!

vivit の技術スタックを全て公開します!!

こんにちは!! vivit で SRE をやっている 宮本 です。

今回は vivit で使用している技術スタックを全部まとめて紹介します 🎉


全体図

vivit 技術スタック

GCPKubernetes マネージドサービスである GKE が中心となっており、マイクロサービス構成となっています。

言語、インフラ含め相当モダンな環境になっているかと思います!!
以下、分野ごとに解説します。

Frontend

TypeScript/Next.js/Vue.js

TypeScript で書かれた Next.js が基本となっています。
Jest, Cypress を使いテストも書いています。

hinata media の一部で Vue.js も使っていますが、今後使用範囲を拡大していく予定は無く、Next.js に統一していく流れです。

Backend

Go/gRPC/GraphQL/Ruby/Ruby on Rails

Go で書かれた gRPC サーバーが基本となっています。
hinata media のみ、バックエンドに Ruby on Rails を使っています。

gRPC サーバーとフロントエンドマイクロサービスの間に BFF として Go で実装された GraphQL サーバーがある構成です。

Infrastructure

Kubernetes/GCP/AWS/Terraform

一部 AWS も使っていますが、ほとんど GCP を利用しています。
GCP の各種サービスを組み合わせて基盤を構築しており、Kubernetes が中心です。

インフラは IaC を実践しており、 Terraform でコード化されています!

その他

  • Datadog
    • ログ管理、サービスのモニタリングなど
    • SRE だけでなく、アプリケーション開発者も使いこなしています
  • GitHub Actions
    • テスト、静的解析、Docker イメージのビルドなどの CI を行っています
  • CiecleCI
    • GitHub Actions ではカバーしきれない部分の CI として、一部 CircleCI も使っています
  • Shopify
    • hinata store は shopify をベースに作られています
  • エディタ/IDE
  • ArgoCD
    • Kubernetes の CD ツールです
    • GitOps については こちら に詳しく記事を書いています
  • Nginx
    • リバースプロキシとして、各マイクロサービスの前に配置しています

さいごに

vivit では、エンジニアを積極採用中です!

ポジションも様々用意しておりますので、今回の記事で書いたような技術スタックに興味を持って頂いた方は、是非カジュアル面談にお越しください。

新卒採用

www.wantedly.com

キャリア採用

www.wantedly.com

www.wantedly.com

www.wantedly.com

皆様とお話できるのを楽しみにしております!!

【23卒】エンジニアの就活した後に考える就活の軸

こんにちは!技術開発部の北條です。

今回は僕が改めて新卒としてエンジニア就活をするならどういったことを重要視するかを考えてみました。 インターンとして5ヶ月経過し、就活の時点で「知っておければ良かった、勉強しておければ良かった」という点があるので紹介します。

尚、この記事はメディアチームでの経験なので他のチームとは異なる部分があるかもしれませんが予めご了承ください。

掲げていた就活の軸

僕は「人生を豊かにするようなサービスを提供する」「ビジネスサイドとの距離が近い開発体制がある」の2点を就活の軸として掲げていました。 vivit.hatenablog.com

大学の授業でC言語を触っていたものの、本格的に勉強を始めたのが大学3年生からなのでWeb系を目指すには時期的に遅い分類になります。

技術力で、企業に貢献することはおろかアピールすることも難しいだろうと思っていました。

そのため、新卒のころからサービス志向のマインドを培い、将来は技術力の高いエンジニアの方々をまとめるPM、PLのような立場で企業に貢献しようと考えていました。

改めて就活するなら

「人生を豊かにするようなサービスを提供する」は変わらないと思います。

「ビジネスサイドとの距離が近い開発体制がある」は軸としては間違っていないのですが、それ以前にエンジニア内での開発体制を確認すべきだと思いました。

加えて下記2点について考慮すべきだったり、考えが変わったので紹介します。

技術について

そもそも技術に対しての知識や経験が乏しいため言語の他に何を見たらよいのかわからなかったのです。 今であれば、開発体制や技術的負債について考えると思います。

もちろんモダンな言語を選ぶに越したことはないのですが、言語のバージョンを古いままにしてないか、リファクタリングを積極的に行う文化があるかなどの技術的負債についての意識が高いかを確認します。

バージョンが古いが故に非推奨のものを使い続けたり、負債の影響でさらに負債を増やすような実装を強いられたりしてはエンジニアとしての成長速度は遅くなってしまいます。

vivitはサービス志向であるもののバージョンアップをしっかりと行ったり、技術的負債を残さない(あるいは後に解消する)という姿勢があります。 サービス志向だけを優先するのではなく、技術に関する姿勢もカジュアル面談などで確認しておきたいポイントだなと思いました。

キャリアプランについて

以前はリーダー的な立ち位置で補佐的にコードを書くというキャリアプランを掲げていました。 しかし、5ヶ月働いてみてやっぱりコードを書くこと自体が好きだなと強く感じています。

今後も変わるかもしれませんが、今はビジネス視点も持ったスペシャリストになりたいと思うようになりました。

キャリアプランとは正確に行き着くための地図ではなく、方向を示す羅針盤なのだと考えれば精神的にも余裕のある就活を送れたのではと思いました…笑

キャリアプランに正解はありませんし、大人達も答えを知っている訳ではありません。 そのため就活中はキャリアプランの内容ではなく、なぜそれを掲げているのか抽象化できていることが重要だと感じました。

最後に

技術とキャリアプランについてはvivitのインターンだからこそ感じたものです。

僕は50社程度しか会社を見ていませんし比較できるほどの経験はないのですが、大変満足のいく就活ができたと思っています。

vivitも会社として完璧ではありません。しかし、インターン生でありながら組織の改善に携わることができるのはベンチャーならではだと思うので、より早く貢献できるように引き続き頑張ります!