LayerX エンジニアブログ

LayerX の エンジニアブログです。

兼業する技術

ドーモ LayerX CTO室(兼 Fintech事業部)のken5scalです。

こちらは【LayerX Advent Calender 2021】の36日目の記事です。昨日の記事はこちら

専任として何かに集中できることに越したことはありません。が、時代は崖から飛び降りながら飛行機を組み立てる大攻勢時代、なかなかそうも行きません。 自分もFintech事業部のなかでALTERNAを実装しつつ、LayerX側で横断的なガードレールやISMSを進めるなど複数PJを持っています。

そういったPJは、素早い実装よりも着実に道を舗装する性格の施策であるケースが多いので、継続が重要です。 そして、継続の一番の大敵は、日々の忙しさに起因するダレや怠けです。面倒臭いと思った瞬間に試合終了です。 本エントリでは兼業をしつつ、ちゃんと進捗を出すジツについて記述します。

兼業仲間を増やす

まずは仲間を増やしましょう。上司にも相談し、体制を整備することで、全体のスループットを上げると同時に、「貴重な時間をもらっている」といういい意味での精神的な緊張により、ダレをなくします。

タスクを分解する

タスクを分解しまくり、面倒臭くないと思う範囲まで小さくしましょう。それこそが頭出しやたたき台になり、建設的な議論と質の良いアウトプットにつながります。 また進める上で、遠くも近すぎもないチェックポイントを置くことで、ちゃんと進んでる感を常に持てるようにします。日々(か週ごと)の小さな達成感がダレや倦怠感を減らします。

毎日進める

毎日一時間でもいいので進めます。進捗を出してゴールに近づいてることを意識し、モチベーションを維持しましょう。 仕様やAPIの理解が進み、それは資産になります。振り返ったら驚くほどの複利効果が貯まるまで、資産を積み上げていきましょう。

薄く広く実装する

特定のコンポーネントを深掘りすぎると、事情や仕様が変わった時に大変です。 CI/CDのように開発とリリース間の距離を短くできるようなコンポーネントは、大きく進捗に影響します。 雑でもいいので、サクッと作ってしまいましょう

Dev環境を用意する

「ローカルからAPI叩きたいけど、APIキーを置いても大丈夫かな?」というような心理的障壁は、即怠惰な気持ちにつながります。 いかに「別に問題が起こっても、本番環境から隔離されてるから大丈夫」と安心感をもてるように、git-secrets, secretlintのようなものをリポジトリやCIに入れてしまいましょう。 これで自信を持って開発に集中できますね。当社の場合、Azure ADやGoogle Workspace、slackなどの社内サービスにもDev環境を用意しています。

CTO室では、重要サービスの保護活動や、脅威モデリング、プライバシー体制の構築等に勤しんでいます。 また、その傍ら実際のプロダクトにもがっつり関わり、ゆくゆくはその知見をもとにした爆速な構築体験を提供できるようになりたいものです。 さまざまな技術や分野に関わっていきたい方は、是非、ご検討ください。

layerx.page.link

次回はマーケのgokanさんです。

暇だしDevSecOpsやってみた - CodePipeline Now and Then

こんにちは。LayerX Fintech事業部(兼CTO室)のken5scalです。

こちらは【LayerX Advent Calender 2021】の35日目の記事です。昨日の記事はこちら

Fintech事業部としての私は、三井物産様らと当社で立ち上げた三井物産デジタル・アセットマネジメントに出向しています。 そちらで先日リリースしたプロ投資家様向けのオンライン投資サービス「ALTERNA(オルタナ)」におけるCI/CDのお話をさせていただきます。 簡単にサマると、Day1リリース時点でContinuous Integration と Continuous DeliveryをCodeCodePipelineをベースにしただけなのですが、 それまでにどういう技術選定を検討したかお話しうようと思います。なお、QAも含めてprdにリリースするという意味でのDeploymentについては未構成です。

なお本件は AWS Startup Tech Meetup Online #9 - Security - connpass で話た内容です。

speakerdeck.com

Alterna DevOps体制

Alterna開発と言いましたが、開発が開始してからは(ほぼ)Ops周りに張りつき、スプリントに参加し以下の対応に携わっていました。

  • 開発スプリントで実現したい機能にあわせたリスク対応
  • FISCや監督指針に照らしあわした要件の洗い出しとタスク化
  • AWS Orgの構成から、dev/stg/prdにおけるインフラ構成(含むコンテナ化)
  • Datadogによる監視と通知構成
  • Ops周りの運用, フローの作成CI/CDの整備

もちろんTerraform化済みです。

ver0.1 CI/CD

その中でもCI/CDは開発からデプロイまでの作業効率化をするだけでなく、 開発者が作ったコードをお客様に提供するサービスに昇華する意味でも重要なコンポーネントです。 また金融機関で重要な開発と運用の分離を実装する意味でも、IAMやSecret管理などでの非機能要件も考慮せねばならないため、初期段階から取り組んでいました。 その際の初期構成がこの通りです。

f:id:kengoscal:20211118021205p:plain

AWSではECS構成を取っているのですが、ECRのためにわざわざスコープが難しくなりがちな共有アカウントを建てたくありませんでした。 そのため、丁度リリースされたGithub Container Registryを活用したろ!という発想で構成したものがこちらです。この場合、コンテナビルドに続くDeliveryもGithub Actionで完結します。シンプルです。

しかし、そのためにはGitHub内でAWS IAMユーザーのシークレットを管理せねばなりません。 つまり最終的なプロダクトのセキュリティレベルが、Githubのセキュリティレベルに依存することになってしまいますが、Githubは、その権限設計の大雑把さなどセキュリティに少々不安があります(特に非Enterprise環境は...)。

ver0.2 CI/CD

不安を払拭して、デプロイをAWSに移した構成がこちらです。

f:id:kengoscal:20211118022942p:plain

こちらではGithub Action内でコンテナイメージをビルドし、Github Container Registryにpushする点については変わりはありません。ただ、AWSのCodepipelineを活用してDeployをしています。

最初はこれでもよかったのですが、スプリントが進むにつれて、dev/stg/prdで管理アプリを構築したり、あるいは他プロジェクトが始まるなどあり、徐々にGithubのパーソナルトークン管理に疲弊してきました。ログも出ないし...。

ver1.0 CI/CD

で、最終的に落ち着いた構成がこちらです。

f:id:kengoscal:20211118023733p:plain

各環境にecrとcodepipelineができました。Github Actionはテストやリンターなどのチェックのみに利用されます。 この段階でもまだ共有化はしていません。結局のところ、SREやOps関連のチームもまだないため、この段階でさらに管理対象を増やすのは避けたかったためになります。ArtifactsやDelivery方式も含めて今後検討する必要があると思います。

結びに

上記のようなOpsを最適化するにあたって、Fintech事業部は規模を増やしていかねばなりません。 是非、ご興味がある方は下記のページをご覧ください(募集ポジションも掲載されているので是非)

layerx.notion.site

なお、中のことがワカル、オンラインイベントがちょうど公開されておりますのでこちらもぜひです

layerx.connpass.com

明日はCTO室のken5scal さんが何か書く予定です

インフラ未経験者にも優しいインフラ設計

こちらは【LayerX Advent Calender 2021】の29日目の記事です。昨日の記事はこちら。

note.layerx.co.jp

明日は@SATOKEN_Xさんがなにかを書いてくれる予定です。

こんにちは、LayerX インボイス・ワークフローのOCRの開発を主に担当しているtomoakiです。最近は自宅でパンを焼くことにハマっています。いろんな種類の小麦粉で試したいのですが、kg単位でしか買えない小麦粉がほとんで、なかなか気軽に買えないのが最近の悩みです。

さて、最近社内用のデータ管理基盤のインフラを構築する機会を頂いたのですが、その時LayerXサービス群のインフラ設計が非常に再利用しやすく、インフラ未経験の私でも他の業務も並行で行いつつ2週間ほどでサービスインフラを構築できたので、初心者目線で「ここわかりやすくて助かった!」みたいなところを紹介していきたいと思います。なお弊社のインフラはAWSを利用しています。

TL; DR

  • LayerXサービス群のインフラはTerraformで再利用しやすく管理されており、インフラ未経験のエンジニアでもサクッとサービスインフラを構築できた。
  • 例えば、Terraformのディレクトリ・ファイルの構造や、環境変数の使い方が工夫されていたので再利用しやすかった。
  • また、タグを活用し、各リソースがどのレポジトリで管理され、どのような役割を持っているかが定義してあったので、初見でも既存の設計が理解しやすかった。

私がどれくらいの初心者だったかと言いますと、お恥ずかしながらTerraformに関してはplanとapplyすら知らない状態で、なんとなく「コードでインフラを管理できるもの」くらいの認識でした。AWSも業務でたまにS3にあるファイルを見るくらいで他のサービスはほとんど知らない状態でした。

なので以下で紹介するtipsが、今後サービスのインフラを触れるエンジニアを増やしていきたいぞという方のお役に立てば嬉しいなと思っています。

LayerXサービス群のインフラ

まず初めに、LayerXサービス群のインフラの設計を紹介したいと思います。 インフラの構成図はざっくり以下のようになっています。

f:id:tomoaki25:20211109145011j:plain

詳しくはこちらのブログをご覧ください。

tech.layerx.co.jp

さて、今回私が作ることになった社内用のデータ管理基盤のインフラの構成図は以下のような感じです。

f:id:tomoaki25:20211109152029p:plain

かなり、LayerXサービス群から流用できそうなところが多そうなのが見て取れます。 それでは、実際に構築してみて再利用しやすかった点を紹介していきたいと思います。

ほぼ全てのリソースをコードで管理してるから再利用しやすい

まずはやっぱりこれですね。同じリソースを定義するときは、本当にコピペするだけで済むので、えっこれだけで本当にいいの?と一瞬不安になるレベルですが、全く問題ありません。

後ほど紹介しますが、localやvarを活用することで、本当に一文字も変える必要なくコピペできるのでビックリしました。 手動で設定したのは、Secret Managerの値くらいです。(Secret Managerのリソース自体はTerraformで管理しています)

Terraformのディレクトリの構造がとてもシンプル

f:id:tomoaki25:20211101093404j:plain

Terraformのディレクトリの構造はフラットに配置しているので、どこに何が定義されているかで迷ったことはありません。ファイル名も(provider)_(resource)_(target).tfという規則で命名しているので、どこにどのリソースを定義すればいいかが一目瞭然でした。

本番環境と開発環境でAWSのアカウントを分けてるので安心して開発できる

初心者がインフラを触るときに一番怖いのは、変更してはいけない箇所をうっかりいじってしまい顧客影響が出てしまうことです。デプロイで失敗したときなど、開発環境だったからこそ安心してデバッグすることができました。

環境特有やプロジェクト固有の定数は全て環境変数で定義されてるので同じリソースを作るときはコピペするだけで済む

f:id:tomoaki25:20211109192842p:plain

環境変数は.tfvarsファイルで定義してenvディレクトリに配置し、Terraformコマンド実行時に引数で指定します。また、プロジェクト固有の定数はlocals.tfで定義しています。 したがって、既存のリソースと同じものを作るときはをコピペするだけで済みます。本当に何もいじる必要はありません。

リソースのセキュリティレベルや対応するTerraformのレポジトリをタグで示すことで、GUIをいじるときに安心

f:id:tomoaki25:20211109153633j:plain

初心者だと特にそうですが、これ勝手にいじっていいのかな、、、と不安になります。 必要とするセキュリティのレベルをタグで管理したり、どのリソースがどのレポジトリで管理されているかをタグで管理することで、うっかり意図しない変更をしてしまうのを防ぎやすくなります。

どのリソースがどのレポジトリで管理されているかは、レポジトリ内の全リソースに共通して付与するタグとしてlocals.tfでmanaged_by = "レポジトリ名"のように定義し、各リソース定義のdescriptionに代入しています。 GUI上でリソースの設定を変更する時も「このリソースはこのレポジトリで管理されている」ということが一目見てわかるので非常に助かります。

また、セキュリティレベルを管理するタグとしてconfidentialityというものを定義し、各レポジトリをsensitive, confidencial, private, proprietary, publicのいずれかに区分しています。 confidentialなものについては権限周りの設定等を変更する際に一層慎重に行おうという意識が生まれたり、少しでも自信がない部分はレビューしてもらおうという意識が生まれます。

タグの活用のメリットに関しては上記以外にもいくつかありますので、興味ある方は是非こちらをご覧ください。 speakerdeck.com

終わりに

LayerXではエンジニアを積極的に採用しています。私のように、フルスタックじゃないエンジニアでも新しい領域にチャレンジし、自分のスキルを上げられる環境がありますので、「LayerXはフルスタックじゃないとダメなんでしょ、、、?」と思っている方もお気軽にご応募ください!最近はフロント・バックエンドで分けた募集要項も公開しています! herp.careers

応募する前にまずは中の人と話してみたいという方は是非カジュアル面談をさせてください! meety.net

また、インフラについてもっと詳しい話が聞きたいよという方はこちらのmeetyがおすすめです! meety.net

meety.net

meety.net

また、LayerXの守護神ことshnjtkさんが先日awsj communityさんのイベントにて、チームでのインフラ管理における要点について発表していますので是非ご覧ください!(43:25~) youtu.be スライドはこちら

speakerdeck.com

Terraform Module はいつ使う?

こんにちは、LayerX で主にインフラを担当している高江です。
この記事は LayerX Advent Calender 2021 の記事です。

先日、JAWS-UG コンテナ支部さんのイベントに登壇させていただき、LayerX インボイスの DevOps について発表いたしました。

jawsug-container.connpass.com

動画はコチラ

www.youtube.com

このセッションでは、

  • 開発スピードを落とさないようインフラ開発をボトルネックにしない
  • 本番環境で使用するリソースは IaC で管理する

ことを目的として、チーム全体でインフラを管理するために

  • IaC リポジトリのディレクトリ構成やファイルの命名規則
  • リソースのタグ付け
  • terraform plan 実行時の差分をなくす工夫
  • CI/CD の自動化
  • IaC ツールの使い分け

などの実践的な方法についてご紹介しました。

発表後に視聴者の方から

f:id:shnjtk:20211103170527p:plain

という質問をいただきましたので、今日はこれに対する回答として、Terraform Module はいつどのように使うのか?について自分なりの考えを述べたいと思います。

Terraform Module とは

公式サイトでは次のように説明されています。

A module is a container for multiple resources that are used together.

ざっくり解説すると、お互いに関連する複数のリソースをまとめて1つの単位として管理するための仕組みです。一般的なプログラミング言語におけるライブラリやパッケージのようなものをイメージしていただければよいかと思います。
使い方としては、Module として定義したファイル群をある場所で管理しておき、その Module を利用する側は module ブロック内の source でその場所を参照するという感じになります。

公式サイトに掲載されている Module の利用例:

module "consul" {
  source = "hashicorp/consul/aws"
  version = "0.0.5"

  servers = 3
}

「ある場所」と書きましたが、公式サイトに記載されているように、様々な場所を指定することが可能です。 ここでは次の3つに分類します。それぞれの名前は説明のために便宜上付けたもので、公式な名称でありませんのでご注意ください。

  • Local Module
  • 3rd-party Module
  • Proprietary Remote Module

以降では、それぞれの Module について概要や長所・短所について説明します。なお、前提として「チーム全員でインフラを管理する」という観点での考察になります。

ちなみに、作業ディレクトリ直下も Module という扱いで、これは Root Module と呼びます。

Every Terraform configuration has at least one module, known as its root module, which consists of the resources defined in the .tf files in the main working directory.

Local Module

1つの IaC リポジトリ内で Module を定義して使用するパターンです。

最初の例として、2つのサービスからなるプロダクトのインフラを、各サービスごとに Module として管理する場合のリポジトリ構造を以下に示します。

LocalModule1
├── main.tf
├── modules
│   ├── serviceA
│   │   ├── aws_cloudwatch_logs.tf
│   │   ├── aws_ecs.tf
│   │   ├── aws_iam_ecs.tf
│   │   ├── aws_lb.tf
│   │   ├── aws_rds.tf
│   │   ├── aws_route53.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── serviceB
│       ├── aws_cloudwatch_event.tf
│       ├── aws_lambda.tf
│       ├── aws_sqs.tf
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
└── variables.tf

このパターンでは、以下の長所と短所が挙げられます。

  • 長所
    • サービスごとに定義場所がはっきりしているので、インフラを変更したい場合にどこに手を入れればいいかが明確
    • リソース名の重複を気にしなくてよい (一種のネームスペースとして利用できる)
  • 短所
    • serviceA と serviceB で相互にリソースを参照するようになると管理がつらい
      • 例えば、serviceA で定義されている LB の URL を serviceB で参照する場合など
      • serviceA の outputs.tf で LB の URL を外出し → ディレクトリトップの main.tf で serviceB の Module を呼び出す際に引数として与える、という流れになる
      • Terraform Module 間のデータの受け渡し方法など、Module の仕組みについて詳しくないと手を入れにくい

短所がマイクロサービスの境界を見誤ってつらくなるパターンに似ていますが、マイクロサービスは最初から意図的にするものではなく開発が進む中で自然な境界が見えてきてから分割するもの、という考え方からすると、少なくとも初手でこのディレクトリ構造にするのはちょっとリスクがあるかなと個人的には思います。
このパターンにするなら、serviceA と serviceB でリポジトリを分け、必要なリソースは Data Source として参照した方がいいかと思います。(循環参照にならないように依存関係には注意する必要がありますが)

2つ目の例として、環境変数(データ)とリソース定義(テンプレート)を分離するパターンのリポジトリ構造を示します。 最初の例を少し拡張した感じですが、こちらのパターンの方が Module の使い方を検索したときによく出てくるかなという印象です。

LocalModule2
├── envs
│   ├── dev
│   │   ├── main.tf
│   │   └── variables.tf
│   └── prd
│       ├── main.tf
│       └── variables.tf
└── modules
    └── serviceA
        ├── aws_cloudwatch_logs.tf
        ├── aws_ecs.tf
        ├── aws_iam_ecs.tf
        ├── aws_lb.tf
        ├── aws_rds.tf
        ├── aws_route53.tf
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

このパターンの長所と短所は以下のようになります。

  • 長所
    • データとテンプレートが分かれているので、「ある環境固有のパラメータを変えたいのか」「全環境に共通の構成を変えたいのか」に応じて手を入れる先が明確
  • 短所
    • 最初の例と同じく、modules 以下に複数の Module が配置され Module 間の参照が発生すると管理がつらくなる

データとテンプレートが分離されているので見通しはいいのですが、Module をどのように切り出すかという点については最初の例と同じ課題を抱えています。 また、この例のように modules 以下の Module 数が1つしかないシンプルな構成なのであれば、下記スライド(p.9)でご紹介したフラットな構成にして環境変数を env ディレクトリ以下に配置するシンプルな構成の方がいいのではと思います。

speakerdeck.com

以上の理由により、LayerX インボイスではこの Local Module は使用していません。

余談ですが、ここではある1つのプロダクトの環境別の情報を envs 以下に配置したケースについて考察しましたが、AWS マルチアカウント運用で、共通して設定したい課金管理やガードレールのような構成を適用する場合には向いていると思います。(Module 間の参照は起きにくいという前提)
その場合、envs 以下には各 AWS アカウントの値(AWS アカウント ID など)を定義する形になります。

3rd-party Module

第三者により公開されている Module を使用するパターンです。 Terraform Registry のほか、GitHub や Bitbucket などで公開されている Module を利用するケースが該当します。

このパターンの長所と短所は以下になります。

  • 長所
    • よくある構成について定義済みのものを利用することで、自分で開発する手間を省くことができる
  • 短所
    • プロダクト固有の構成変更が困難
    • リポジトリを消されるとある日突然使えなくなる

プロトタイプ開発などで、よくある構成のインフラを早く楽に作りたい場合には適していると思いますが、本格的に運用するサービスについては細かい変更が難しいことから向いてないと思います。 よって、LayerX インボイスではこの 3rd-party Module も使用していません。

なお、例えば GitHub で公開されている Module であれば fork すれば上記短所は解決できますが、それは次に説明する Proprietary Remote Module に分類されます。

Proprietary Remote Module

自分の都合で変更可能な、リモートで管理されている Module を使用するパターンです。 公開か非公開かは問いません。

LayerX インボイスでは、この Proprietary Remote Module を使用しています。 以降では、このパターンの Module を使用するに至った経緯からご説明いたします。

LayerX インボイスのサービス構成

LayerX が開発・提供するサービスには、請求書を AI-OCR で処理して各種会計サービスと連携する「LayerX インボイス」のほか、購買申請や支払申請などの申請・承認フローを管理する「LayerX ワークフロー」や、ちょうど先日発表した改正電子帳簿保存法に対応した書類保管サービスである「LayerX 電子帳簿保存」など、複数のプロダクトが存在します。

これらのプロダクトにおいて、「IaC のコードを実行してインフラを構成管理する」という機能、つまり IaC の CI/CD パイプラインは共通して必要なものであり、かつそれほど頻繁に変更されるものではありません。

リポジトリ直下に .tf ファイルをフラットに配置し、各プロダクトごとに個別の IaC リポジトリを作って管理するこれまでの方針の場合、新プロダクトの開発に伴ってパイプライン関連のコードのコピペが多く発生し、共通して構成を変更したい場合でもファイルが複数箇所に分かれていることによって DRY ではなく、コードの管理が煩雑になるという課題がでてきました。

そこで、IaC の CI/CD パイプラインを Module 化し、各プロダクトが共通して利用するようにいたしました。
この判断の根拠としては、コードを一箇所で定義して再利用性を高めるというだけでなく、アプリ開発エンジニアが IaC の CI/CD パイプラインに変更を加えることは基本的にないため、Module 化したとしてもチーム全員でインフラを管理するという観点からは影響はほぼないと考えられることも一因として挙げられます。

IaC の CI/CD Module

現在の CI/CD パイプラインの構成図と Module 構成を以下に示します。

f:id:shnjtk:20211103203006p:plain
IaC CI/CD パイプライン

IaCModuleRepository
└── aws
    └── infra-deploy-pipeline
        ├── aws_cloudwatch_logs.tf
        ├── aws_codebuild.tf
        ├── aws_codepipeline.tf
        ├── aws_codestar_notifications.tf
        ├── aws_iam.tf
        ├── aws_s3.tf
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

このパイプラインがやっていることは単純で、

  • GitHub にある IaC リポジトリからコードをチェックアウト
  • CodeBuild で terraform apply を実行

というフローなのですが、構成するリソースとしては CodePipeline や CodeBuild だけでなく

  • CodePipeline および CodeBuild に付与する IAM Role
  • チェックアウトしたコードを CodeBuild に渡すアーティファクトストアとしての S3 バケット
  • CodeBuild のログを保管するための CloudWatch Logs
  • パイプライン処理の開始・終了を Slack に通知するための CodeStar Connection

なども必要になります。
これらのコードを Module としてまとめて管理することで、各プロダクトの IaC リポジトリでは、以下のように Module ブロックを追加するだけでよくなります。

module "infra_deploy_pipeline" {
  source = "git@github.com:(GitHub user name)/(IaC Module repo name)//aws/infra-deploy-pipeline"

  tf_version                = var.tf_version
  service_id                = local.service_id
  managed_by                = local.managed_by
  env                       = var.env
  repository_id             ="${local.gh_org_name}/${local.gh_repo_name}"
  branch_name               = var.branch_name
  codestar_connection_arn   = var.aws_codestar_connection_arn
  aws_chatbot_arn           = var.aws_chatbot_slack_arn
}

Module に与えるインプットとしては主に以下になります。

  • Terraform のバージョン
  • チェックアウトするリポジトリ名やブランチ名
  • CodeStar Source Connection ARN
  • Slack 通知のための AWS Chatbot ARN

CodeStar Source Connection や Chatbot は、AWS アカウント内で1つ作成しそれを共用しているため、その既存リソースの ARN を指定するようにしています。 また、 source 指定では varlocal などの変数は使えないため、GitHub ユーザー名やリポジトリ名をハードコードすることになります。

なお、この Module はプライベートリポジトリで管理しているため上記のような source 指定となりますが、リポジトリ内のサブディレクトリを指定する時は、リポジトリ名の後の / は2つ必要になります。((IaC Module repo name)//(directory)

このような Module を作成することで、今後新たなプロダクトを開発する際に IaC の CI/CD パイプラインが必要になっても、上記 Module ブロックを記述するだけで他のプロダクトと同じパイプラインをすぐに構築することができ、コードも一箇所で管理されるため DRY になります。

まとめ

今回は、チーム全員でインフラを管理するという前提のもと、Terraform Module の使用例と LayerX インボイスではどのように Module を使用しているかについてご紹介いたしました。 まとめると以下となります。

  • Local Module は初手で使用するのは避ける
  • 3rd-party Module は本格運用するサービスで使用するのは避ける
  • Proprietary Remote Module はプロダクトラインナップが増えてコードのコピペが増えてきたら使用を検討する
    • プロジェクト全体で共通の構成、かつ頻繁に変更されないリソース群は Module 化の良い対象となる
    • アプリ開発エンジニアも触る可能性があるリソースについては無理に Module 化しなくてもよい

これまでにも色々な Module の使い方を試してみて、現時点でこれがいいのではという私自身の見解になります。Module の使用について悩まれている方のヒントになれば幸いです。

We are hiring!!

LayerX インボイスでは、サービスの拡大に伴いインフラもこれからどんどん発展していきますので、成長する SaaS のインフラ開発スキルを磨きたい、チームの規模に合わせた実践的で効率的なインフラ管理を追求したいなど、インフラ開発に興味がある方はぜひ一緒に開発しましょう! もちろん、インフラだけでなくアプリやQA、CSなどサービス開発に関わる全ての職種で積極的に採用中ですので、お気軽にご連絡ください!

また、私が所属する CTO 室では、社内全体のインフラ管理やセキュリティ向上のための様々な施策に取り組んでいます。 今回ご紹介したような Terraform の活用方法をはじめとして、定期的にメンバーが集まって上手くいったことや新しく出てきた課題などを日々情報共有してレベルアップに努めておりますので、 クラウド管理ツールやクラウドプロバイダーを活用して組織の活動をテクノロジーで支えるという「屋台骨エンジニア」に興味がある方はぜひこちらの求人もご覧ください。

herp.careers

LayerX の事業やチームについて、もっと知りたい方はこちら

layerx.notion.site

LayerX のメンバーとカジュアルに話してみたい方はこちら

meety.net

LayerX の募集ポジション一覧はこちら

herp.careers

【イベント資料まとめ】LayerXから2名のエンジニアがAWS Dev Day Online Japan 2021に登壇しました! #AWSDevDay

f:id:shun_tak:20211103162355j:plain

こんにちは!2021年9月末に開催されたAWS Dev Dayですが、LayerXから2名のエンジニアが登壇しました。スライドと当日の動画が公開されたので、本ブログでも紹介します!

なお、この記事は10月からフライングで始めているLayerX Advent Calender 2021の記事です。

LayerXインボイスのAI-OCRを支える非同期処理アーキテクチャ

スピーカー:高際 隼 (株式会社LayerX DX事業部 AI-OCRチーム Lead)

動画

www.youtube.com

スライド

スライド抜粋

f:id:shun_tak:20211103154610j:plain f:id:shun_tak:20211103154619j:plain f:id:shun_tak:20211103154631j:plain f:id:shun_tak:20211103154641j:plain f:id:shun_tak:20211103154652j:plain f:id:shun_tak:20211103154701j:plain f:id:shun_tak:20211103154709j:plain

やはりタグ タグは全てを解決する - Tagging Everything

スピーカー:鈴木 研吾 (株式会社LayerX シニアセキュリティアーキテクト)

動画

www.youtube.com

スライド

speakerdeck.com https://pages.awscloud.com/rs/112-TZM-766/images/I-1__AWSDevDay2021.pdf

スライド抜粋

f:id:shun_tak:20211103155720j:plain f:id:shun_tak:20211103155737j:plain f:id:shun_tak:20211103155755j:plain f:id:shun_tak:20211103155816j:plain f:id:shun_tak:20211103155830j:plain f:id:shun_tak:20211103155845j:plain f:id:shun_tak:20211103155859j:plain f:id:shun_tak:20211103155914j:plain f:id:shun_tak:20211103155948j:plain

【GraphQL × Go】gqlgenの基本構成とオーバーフェッチを防ぐmodel resolverの実装

こんにちは。SaaS事業でLayerX ワークフローの開発を担当している@sh_komineです。 この記事は、LayerX Advent Calender 2021の16日目の記事です。

LayerX ワークフローではGoとGraphQLをフル活用して開発を行なっています。

www.layerx.jp

GraphQLの良さはいろいろと語られていますが、「Goで実際にどう実装するんだ?」と言うところは、gqlgenの簡潔なGet Startedがあるくらいでなかなか手で動かさないと理解できないなという思いがありましたので、graphqlのプロジェクトの基本構成に触れながら、オーバーフェッチを防ぐ実装の仕方について書いていきたいと思います。

本記事は実際の事例ではなく、gqlgen 初学者の全体把握、gqlgenの仕組みについての理解に焦点を当てた記事になります。

以下の流れで話をします。

  1. graphqlの基本構成
  2. オーバーフェッチングを防ぐためのmodel resolverの実装
  3. 補足 graph/schema.graphqls の分割

今日はしない話

フロントエンドと組み合わせた全体の話や、DBなどの処理も含めた全体を通した話はしません。 実装例については、尊敬する同僚の@mosaさんや@anagoさんが過去に書いた記事がありますので、そちらをご覧ください。

  • RESTとGraphQLでの実装の違いについて解説した記事 tech.layerx.co.jp
  • GraphQLを用いたフロントエンドからバックエンドまで開発の流れを紹介した記事 tech.layerx.co.jp

1. graphqlの基本構成

graphqlの基本構成について理解するために、以下の流れで見ていきます。 運用しているとどんどんファイルは大きくなり、全体像の把握が難しくなってくるので、一番最初のシンプルな構成から内容を見ていきます。

  • 1-1. スケルトンプロジェクトを作る
  • 1-2. 生成されたファイルのそれぞれの役割について

1-1. スケルトンプロジェクトを作る

Getting Startedに倣い、Goのプロジェクト内でgqlgenをインストールして、スケルトンプロジェクト(雛形)を作成します。 (多分、どのプロジェクトでも導入の際はここから始まると思います。)

# go moduleの作成
$ mkdir gqlgen-todos
$ cd gqlgen-todos
$ go mod init github.com/[username]/gqlgen-todos

# gqlgenのスケルトンプロジェクトを作成
$ go get github.com/99designs/gqlgen
$ go run github.com/99designs/gqlgen init

スケルトンプロジェクトは以下のように作成されます。

├── go.mod
├── go.sum
├── gqlgen.yml               // コード生成の設定ファイル
├── graph
│   ├── generated            // 自動生成されたパッケージ(基本的にいじらない)
│   │   └── generated.go
│   ├── model                // Goで実装したgraph model用のパッケージ(自動生成されたファイルと自分でもファイルを定義することが可能)
│   │   └── models_gen.go
│   ├── resolver.go          // ルートのresolverの型定義ファイル. 再生成で上書きされない。
│   ├── schema.graphqls      // GraphQLのスキーマ定義ファイル. 実装者が好きに分割してもOK
│   └── schema.resolvers.go  // schema.graphqlから生成されたresolverの実装ファイル
└── server.go                // アプリへのエントリポイント. 再生成で上書きされない。

めちゃくちゃ簡単に雛形ができました。これがgqlgenのシンプルな基本構成になります。 rootの構成としては、以下のようになっています。

  • gqlgen.yml: 設定ファイル
  • graph: gqlgenの開発で利用するファイルがいろいろと含まれるパッケージ(ここのファイルについては後述)
  • server.go: main関数

次は生成されたファイルのそれぞれの役割について見ていきます。

1-2. 生成されたファイルのそれぞれの役割について

graph/schema.graphqls : GraphQLのスキーマ定義ファイル

graph/schema.graphqls はGraphQLのスキーマ定義ファイルで、APIエンドポイントとその型を管理するのが役割です。 スケルトンプロジェクトでは以下のように生成されます。 以後、自分が書き加えた箇所には ⭐️ をつけていきます。

# ⭐️  type: 基本のオブジェクトタイプ
type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

type User {
  id: ID!
  name: String!
}

# ⭐️  type Query: データフェッチ系のエンドポイント定義
type Query {
  todos: [Todo!]!
}

# ⭐️  input: Mutaion用のオブジェクト型の定義
input NewTodo {
  text: String!
  userId: String!
}

# ⭐️  type Mutation: Serversideのデータを修正する処理のエンドポイント定義
type Mutation {
  createTodo(input: NewTodo!): Todo!
}

gqlgenでは基本的にこの graph/schema.graphqlsというGraphQLの定義ファイルを編集し、

gqlgen

というコマンドを実行することで、後述する graph/model/models_gen.gograph/schema.resolvers.go を再生成して開発を進めていくので、全ての起点になるファイルです。 gqlgenの話の前にGraphQLの型定義のより詳しい仕様が知りたい方は公式のドキュメントをご覧ください。

graphql.org

graph/model/models_gen.go : graph/schema.graphqls から自動生成されたGoの型定義ファイル

graph/model/models_gen.gograph/schema.graphqls から自動生成されたGoの型定義ファイルです。 上記の graph/schema.graphqls と見比べると分かりますが、 type Querytype Mutation 以外の type Todo, type Userinput NewTodo のstructが自動生成されています。

// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.

package model

// ⭐️ input NewTodoから生成されたファイル
type NewTodo struct {
    Text   string `json:"text"`
    UserID string `json:"userId"`
}

// ⭐️ type Todoから生成されたファイル
type Todo struct {
    ID   string `json:"id"`
    Text string `json:"text"`
    Done bool   `json:"done"`
    User *User  `json:"user"`
}

// ⭐️ type Userから生成されたファイル
type User struct {
    ID      string  `json:"id"`
    Name    string  `json:"name"`
    Friends []*User `json:"friends"`
}

type Querytype Mutationgraph/model/models_gen.go には作成されません。 graph/schema.resolvers.go に作成されます。

また詳しくは後述しますが、graph/model 配下には自分で定義したGoのモデル定義のファイル *.go を置くことができます。 自分で定義したGoのモデルも gqlgen コマンドで読み込まれ、graph/schema.graphqlsをベースに*.goに足りないFieldが graph/schema.resolvers.go に生成されます。 ひとまずは、graph/model パッケージはGraphQLの型オブジェクトに対応するgo のstructを管理する役割があると覚えてください。

graph/schema.resolvers.go : graph/schema.graphqls ファイルから自動生成されたエンドポイント実装用のGoファイル

graph/schema.resolvers.gograph/schema.graphqls ファイルから自動生成されたエンドポイント実装用のGoファイルです。エンドポイントの管理が役割になります。 最初の状態は以下のように未実装の状態でコードが生成されているので、そこに実装をしていく流れになります。

resolverはよくあるcontrollerやhandlerと役割は同じなので、そこまで難しく考えなくて大丈夫です。 この中の詳細の実装はresolverに処理をベタ書きでも、MVCでもDDDでも実装者の好きに実装していくことができます。

graph/schema.resolvers.go

package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
    "context"
    "fmt"
    "math/rand"

    "github.com/shkomine/gqlgen-todos/graph/generated"
    "github.com/shkomine/gqlgen-todos/graph/model"
)

func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
         // ⭐️ ここにデータフェッチの実装をしていく
        // 例えばこんな処理
        // r.todoRepo.Create(input)
    panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
         // ⭐️ ここにデータ変更の実装をしていく
        // 例えばこんな処理
        // r.todoRepo.List()
    panic(fmt.Errorf("not implemented"))
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

ここで、上記の mutationResolverqueryResolverのコードを見てみると

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

どちらも Resolver の型定義を含んでおり、状態を持つ役割はResolverに集約されています。

graph/resolver.go, server.go : gqlgen コマンドで再生成されないgoパッケージ

Resolver の型定義は graph/resolver.go に定義されていて、状態を集約して管理するのが役割です。 自動生成される graph/schema.resolvers.go から参照されているので、型の名前を変えたり消すことはできませんが、この Resolver は初期生成の後gqlgenコマンドを実行しても再生成されることはないので、このstructに参照し続けたいインスタンス(pointer)などを持たせることができます。 MVCなら各model、その他 repositoryやservice, usecaseのstructを持つような実装になるかと思います。

graph/resolver.go

package graph

import "github.com/shkomine/gqlgen-todos/graph/model"

// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.

type Resolver struct{
  // ⭐️ TODO: ここにUserRepositoryなどのインスタンスを保持する
}

Resolver インスタンスを作成しているのは server.go です。 Goのmain関数が含まれています。 このファイルも最初にスケルトンオブジェクトで作成された後はgqlgenコマンドの実行で再生成されないので、自由に書き換えることができます。

package main

import (
    "log"
    "net/http"
    "os"

    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/playground"
    "github.com/shkomine/gqlgen-todos/graph"
    "github.com/shkomine/gqlgen-todos/graph/generated"
)

const defaultPort = "8080"

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = defaultPort
    }

         // ⭐️ ここでResolverのインスタンスを生成する
    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))

         // ⭐️ graphql playgroundを`/query` に設定
    http.Handle("/", playground.Handler("GraphQL playground", "/query"))
    http.Handle("/query", srv)

    log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

ここまで初期生成されたスケルトンプロジェクトを元にgqlgenの基本構成を説明してきました。 実際に動かしてみるところまでやってみたい方は、さくっと終わるので公式のGetting Startedをやってみてください。 より深く理解ができると思います。

gqlgen.com

次はいよいよオーバーフェッチングを防ぐためのmodel resolverの実装について触れていきます。

2. オーバーフェッチングを防ぐためのmodel resolverの実装

2-1. オーバーフェッチとは

GraphQLでバックエンドのコードをすっきりさせた話でも触れていますが、

オーバーフェッチ = リクエスト元で必要ないのに余分にリソースをフェッチしてしまうこと

です。

gqlgenのREADMEのよくある質問の最初にも「使わないかもしれない子オブジェクトのフェッチを防ぐにはどうしたらいいか? 」という項目が用意されており、GraphQLの重要なテーマであることがわかります。 GraphQLでは通信元のqueryで取得するリソースを制御することができ、より柔軟で効率的な情報取得が可能です。

例えば、上記のGet Startedの例では、Todoの中にUserが含まれていますが、これは、必ず必要とは限りません。

schema.graphqls

type Todo struct {
    ID   string `json:"id"`
    Text string `json:"text"`
    Done bool   `json:"done"`
    User *User  `json:"user"`
}

GraphQLでは情報の取得側がqueryで明示的に指定することによりそのデータが必要かどうかサーバー側に伝えることができます。

userの名前が必要であれば、以下のようにリクエストをします。

query findTodos {
  todos {
    text
    done
    user {
      name
    }
  }
}

結果

{
  "data": {
    "todos": [
      {
        "text": "技術記事を書く",
        "done": false,
        "user": {
          "name": "田中 太郎"
        }
      },
      {
        "text": "実装をする",
        "done": false,
        "user": {
          "name": "佐藤 二郎"
        }
      }
    ]
  }
}

また、userが必要なければ、以下のようにuserを含めずにリクエストをします。

query findTodos {
  todos {
    text
    done
  }
}

結果

{
  "data": {
    "todos": [
      {
        "text": "技術記事を書く",
        "done": false,
      },
      {
        "text": "実装をする",
        "done": false,
      }
    ]
  }
}

gqlgenではこのuserがリクエストに含まれた時だけ呼ばれるメソッドをgraph/schema.resolvers.goに作成することができます。 これによって、無駄なDBフェッチが走らない実装が可能になります。 gqlgenのドキュメントでは、特に名前がつけられていませんが、graph/schema.resolvers.goの中にqueryResolver, mutationResolver 以外の モデルごとのResolver xxxResolver が生成されるので、ここでは勝手にmodelresolverと呼ばせていただきます。(上記の例では todoResolverが生成されます。)

2-2. model resolverの2つの実装方法

gqlgenのREADMEにも書いてありますが、model resolverの実装には2つの方法があります。

  • カスタムモデルを用いた暗黙的な生成
  • gqlgen.ymlへの記載による明示的な生成

Get Startedでは「カスタムモデルを用いた暗黙的な生成」しか触れられていませんでしたし、自分が試した感触でもそちらの方が便利だったので、カスタムモデルを用いた実装方法を強くおすすめします。

カスタムモデルを用いた暗黙的な生成

model packageの説明で少し触れましたが、gqlgenはgraph/schema.graphqls での定義と model/*.go 内の型定義の差分によって graph/schama.resolvers を生成します。

今、graph/schema.graphqls でのTodoの定義は以下の通りです。

graph/schema.graphqls

type Todo struct {
    ID   string `json:"id"`
    Text string `json:"text"`
    Done bool   `json:"done"`
    User *User  `json:"user"`
}

graph/model/models_gen.go 内のTodoを削除し、graph/model/todo.goというファイルにカスタムモデルを定義します。 その際、todo.go 内のUserを丸っと消してみます。

graph/model/models_gen.go

-type Todo struct {
-       ID   string `json:"id"`
-       Text string `json:"text"`
-       Done bool   `json:"done"`
-       User *User  `json:"user"`
-}

graph/model/todo.go

package model

type Todo struct {
    ID   string `json:"id"`
    Text string `json:"text"`
    Done bool   `json:"done"`
}

つづいて、gqlgen コマンドを実行すると、graph/schema.graphqls の型定義をベースにgraph/model/*.goのカスタムモデルで足りないfieldを検知してgraph/schema.resolvers.goにmodel resolverを追加します。

graph/schema.resolvers.go

func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
        // ⭐️ ここにUserのデータフェッチ処理を書く
        // こんなメソッドになる
        // r.userRepo.GetUserByTodoID(obj.ID)
    panic(fmt.Errorf("not implemented"))
}

// ...

// Todo returns generated.TodoResolver implementation.
func (r *Resolver) Todo() generated.TodoResolver { return &todoResolver{r} }

// ...

type todoResolver struct{ *Resolver }

ただ、上記で生成された状態だとTodoのIDでUserの情報を取得しないといけません。 実際の場合はTodoがUserIDを持っていることの方が多いと思います。 そこで、model/todo.goにUserIDを足します。

model/todo.go

package model

type Todo struct {
    ID   string `json:"id"`
    Text string `json:"text"`
    Done bool   `json:"done"`
+   UserID *string `json:"user_id"`
}

これでmodel resolverの中でUserIDを利用できるようになりました。

graph/schema.resolvers.go

func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
        // ⭐️ ここにUserのデータフェッチ処理を書く
        // obj.UserIDが使えるので、こんなメソッドになる
        // r.userRepo.GetUserByID(obj.UserID)
    panic(fmt.Errorf("not implemented"))
}

graph/model/todo.goにUserIDを追加してもgraph/schema.graphqls には変更を入れていないので、GraphQLのインタフェースには変化がありません。 このように外には出したくないが、relationを追加するために必要なパラメータなどを自由に定義できるので、カスタムモデルの実装はおすすめです。

gqlgen.ymlへの記載による明示的な生成

自分もあまり使っていませんが、一応もう一つのmodel resolverの生成方法もご紹介します。 以下のように、 gqlgen.yml に具体的な指定をしても、カスタムモデルの時と同様にresolverを生成することができます。

gqlgen.yml

models:
  Todo:
    fields:
      user:
        resolver: true # force a resolver to be generated

graph/schema.resolvers.go

func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
        // ⭐️ ここにUserのデータフェッチ処理を書く
        // こんなメソッドになる
        // r.userRepo.GetUserByTodoID(obj.ID)
    panic(fmt.Errorf("not implemented"))
}

// ...

// Todo returns generated.TodoResolver implementation.
func (r *Resolver) Todo() generated.TodoResolver { return &todoResolver{r} }

// ...

type todoResolver struct{ *Resolver }

生成できました。 ですが、こちらの方法だとTodoのモデルにUserIDなどのrelationを追加するために必要なパラメータをgoのモデルに追加することができません。 また、Inline config with directives という機能でより詳細に設定できるそうですが、自分が手元で試した際にはうまく動かなかったので、「カスタムモデルを用いた暗黙的な生成」の方を利用することをおすすめします。

2-3. 具体的なmodel resolverの使用例

ここまで model resolverの実装方法について話してきましたが、重要なのはgraph/schema.graphqls の型定義をベースにgraph/model/*.goのカスタムモデルで足りないfieldを検知してgraph/schema.resolvers.goにmodel resolverを追加します。という部分です。 つまりオーバーフェッチがある部分に関しては、カスタムモデルから削除するだけでqueryで指定されなければ呼び出されないmodel resolverを生成することができます。

例えば以下のような場合に便利です。

Todoにタグ情報が複数紐づく場合にmodel resolverを活用

複数のRelationがあるケースです。

schema.graphqls

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
+  tags: [Tag!]!
}

+type Tag {
+  id: ID!
+  name: String!
+}

schema.resolvers.go

func (r *todoResolver) Tags(ctx context.Context, obj *model.Todo) ([]*model.Tag, error) {
    // ⭐️ TodoのIDでタグの一覧を取得する
    r.tagRepo.ListByTodoID(obj.ID)
}

一点注意点としては、 todoResolverのTagsメソッドは、以下のようにTodoを配列で取得する場合、返却するtodoの件数分呼び出されます。 つまり、有名なN+1問題が発生します。

query findTodos {
  todos {
    text
    done
    tags
  }
}

このN+1問題の解消には、dataloaderという仕組みがあり、実際にはそれで実装しているのですが、 話し始めるととても長くなるのでまたの機会にさせていただきます。 ざっくり言うと、データの呼び出しを一定期間待ち合わせた後に一斉にデータフェッチしてくる仕組みです。 気になる方は 「golang graphql dataloader」で調べてみてください。

provileURLを求められない限りは S3 アクセスしないためにmodel resolverを活用

単一のフィールドであってもCloudのインフラにアクセスする必要があるケースです。 relationだけではなく、こういった単一のフィールドであってもオーバーフェッチを避ける事ができます。

schema.graphqls

type User {
  id: ID!
  name: String!
  profileURL: String
}

model/user.go

package model

type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

schema.resolvers.go

func (r *userResolver) ProfileURL(ctx context.Context, obj *model.User) (*string, error) {
         // ⭐️ awsのS3にアクセスして署名付きURLを発行する
    r.userRepo.getSignedProfileURL()
}

3. 補足 graph/schema.graphqls の分割

余談ですが、実プロジェクトを回していると、schema.graphgqsはどんどん大きく膨れていってしまい、メンテナンスが大変になります。 この schema.graphgqs は簡単に分割できて、例えば、

  • query.graphgqs
  • mutation.graphgqs
  • input.graphqls
  • type.graphqls

という分割の仕方をすると、

  • query.resolvers
  • mutation.resolvers
  • input.resolvers
  • type.resolvers

というようにresolversファイルが分割され、全体の見通しをよくすることができます。 この際、model/models_gen.goに関しては分割されません。 ファイルがとても大きくなってきた際には是非試してみてください。

最後に

今日はgqlgenの基本構成とオーバーフェッチを防ぐmodel reolverの実装について書いてみました。 gqlgenの基本的な仕組みの理解の助けになったら嬉しいです。

We are Hiring

LayerXではエンジニアはもちろん、全職種で積極的に採用中です。 ぜひカジュアル面談からでもお話しましょう!

  • LayerXのEntranceBook

LayerXの事業やチームについて、もっと知りたい方はこちら layerx.notion.site

  • LayerXのMeety一覧

LayerXのメンバーと、カジュアルに話してみたい方はこちら meety.net

  • LayerXの募集ポジション一覧

実際に応募してみたい方はこちら herp.careers

LayerXの業務標準端末を決定するまでにやったこと

CTO室の@ken5scal です。

こんにちは、CTO室の@ken5scal です。 この記事はLayerX 2021アドベントカレンダー 14日目の記事です。昨日はcipeさんの 本番稼働でわかった秘匿化技術のチャレンジングなこと でした。明日はSaaS事業部 BizDevのgunchanさんの記事が公開される予定です。

最近、LayerXのサービスやカルチャーについて多く事例として公開されていますが、では、実際に社員がどのような環境で業務をしているかについては、まだ情報が出てなさそうでした。

特に今日において、どのようなラップトップを使えるかは非常に重要なポイントかと思います。本ブログでは当社で利用される標準業務端末について紹介します。

デフォ端末

  • 8コアCPU、8コアGPU、16コアNeural Engineを搭載したApple M1チップ
  • 16GBユニファイドメモリ
  • 1TB SSDストレージ

入社後必要に応じて

  • ビジネス向け Surface Book 3 あるいはSurface Laptop 4
    • 画面: 13.5inch, RAM: 32GB, Disk: 512GB, CPU: Corei8

以上!!

あっさりし過ぎなので、どうこの方針に落ち着いたかを移行で紹介します。

ご興味がある方は是非一読いただけると嬉しいです。

標準端末を決めるまで

スペックとバラエティ

当社では代表取締役が現役で開発をしていることもあってか、かなり業務端末において性能を重視しています。従って、どのような端末であろうと高スペックにすることが可能でした。これは非常にラッキーでした。

一方、端末の種別はかなり人の好みが反映されます。

Mac ProあるいはAirか、軽さを重視した13インチか作業を重視する15インチか、Lenovo派Dell派など、様々な勢力が入り乱れる、さながら戦国の世です。

残念ながら、当社の人的リソースはカツカツです(本当に...!)。その中でアドホックに依頼を受けた場合、どの程度の負荷がかかるかを実体験することで算出しました。以下が、自分が試しに実績のない端末をチェック〜発注までした時の経過時間になります。

  • 対象者からのヒアリング: 2h
  • 要望端末の要件調査(含むヒアリング): 6h~8h
  • 代理店の探索: 1h~3h
  • 代理店へのヒアリング(含む相見積もり): 1h~3h
  • 新規取引申請・購買申請: 1~1.5h
  • 発注: 2w~5w

代表が20分くらいの話を必ずする週次定例では、プロダクトの開発やマーケティングに投資することの一環として採用を拡大することが宣言されました。

そのようなムーブメントの中、上記のような負荷の大きい調達を続けることは合理的でありません。

標準策定

端末の選定・調達・運用の業務も担当するCTO室では、当時の実務環境やヒアリングをもとに、以下の仮定をおきました。

  • 仮定
    • 当社の資料は基本的にGoogle Slidesやスプレッドシートで作られる
    • Mac利用者の比率が高い
    • 未経験者も大体、二週間ほどで慣れる

この過程の元、Macをデフォルト機にするプロポーザルを全社向けに提出しました。その過程および回答・フィードバックはGitHubにまとまっています。

  • 提案

f:id:maasa_1023:20211014175747p:plain - 回答 f:id:maasa_1023:20211014175812p:plain

その結果、次のようにIntelベースでのMacをデフォルト機とすることを決定しました。当時はまだまだm1 Apple Siliconで動作しないアプリケーションも多くありました。現在は解消されているのでm1がデフォルトです。 f:id:maasa_1023:20211014175851p:plain

一方、実際に「やはりWindowsのが扱いやすい」と言うメンバーもいらっしゃいました。そのような声はマジョリティではありませんが真実ですので、特定の期間がすぎてそれでもWindowsが良ければ、Surface Laptopを希望できる制度にしました。

このようにして本ブログの最初に紹介した端末を選定した流れになります。

もちろん端末は運用しなければならないためJamfとIntuneを使っています...が、

本当はガリガリIaCにしてCICDを回すようなソフトウェアによる管理をしたいのですが、全く時間を取れておりません。是非、こう言ったことを一緒に目指してくれる方にご応募いただけますと幸いです。

最後に

LayerXではエンジニアはもちろん、全職種で積極的に採用中です。 ぜひカジュアル面談からでもお話しましょう!