【登壇資料】Go Conference 2021 Autumn に登壇してきました

f:id:kaminashi-developer:20211130104451j:plain

カミナシ・エンジニアの@issei です。

先日行われたGo conference にてgoldスポンサー枠にて登壇してきました。

gocon.jp

kaminashi-developer.hatenablog.jp

本日は、その際のカミナシ側の登壇資料を紹介します。

発表趣旨

カミナシではバックエンドのAPIサーバーの開発にGolangを利用しております。

カミナシからは「ノンデスクワーカー向けノーコードサービスのつらみ・うまみ」というタイトルにて、 デスクレスSaaSというあまり前例の無いサービスにおいてGoをどう利用しているのかについて、発表させていただきました。

speakerdeck.com

カミナシについて

gyazo.com

製造現場等のノンデスクワーカの分野では、まだまだ紙を主役とした人力の業務フローがメインです。 結果としてミスが多くなりがちになり、基幹業務外の仕事に時間を割かれがちになってしまいます。

カミナシでは、このような紙を主体とした業務フローをデジタル化し、ノーコードで作成できる現場管理アプリを提供しています。

木構造な内部データ

gyazo.com

ノーコードサービスゆえ、内部データが木構造で定義されており、これによりサービス内でのloop処理や条件分岐処理等をユーザが直感的に使用できるようになっています。

これにより、ユーザー自身がノーコードで独自のワークフローを作成・編集できるようになっています。

開発におけるうまみ

Goをサービスに用いることで当社が開発を通じて感じたうまみを紹介しました。

多階層テーブルと静的型付けとの相性

gyazo.com

多階層なテーブルを扱うサービスを開発する上でのGoの静的型付けの特性から受ける恩恵について紹介しました。

他言語からの敷居の低さ

gyazo.com

当社エンジニアの入社前の使用言語を例にGo未経験から実務をこなすまでの敷居の低さの恩恵を紹介しました。

開発におけるつらみ

Goをサービスに用いることで当社が開発を通じて感じたつらみを紹介しました。

カミナシでは、リレーションを多くもつデータ構造ゆえ、gormに関わるつらみを中心に紹介しました。

コード行数の増加

gyazo.com

テーブル間のリレーションが多い分、preloadを多用しがちになるほか、 (genericsがないことからの) switch文やforloopの多用、によって可読性が落ちてしまうつらみを紹介しました。

RDBでの性能低下

gyazo.com

上記で述べた通りpreloadを多用しているので、ORM側で大量のクエリを発行してしまい結果、 application⇄DB間の通信回数が増加することでRDBでの性能低下につながってしまったつらみを紹介しました。

上記への対策として、

  • application側では、ORMから一部、テーブル結合を行うような生クエリに書き換えを実施
  • DB側では、外部キーに対するindexの追加を実施

することで、両者間の通信速度が改善された事例を併せて紹介しました。

テストデータの用意

gyazo.com

多階層なデータ構造ゆえ、テストデータをJSONにて愚直に書いてしまうと用意に膨大な時間がかかるため、自動でエンティティを生成できるような外部モジュール ( factory-go )を使用した事例を紹介しました。

まとめ

現在カミナシではデスクレスSaaSというあまり前例の無いサービス開発をしており、やりがいがある一方で、今回共有した通りまだまだ多階層なテーブルデータを扱うための課題感があるのが現状です。

これらの課題に一緒に取り組んでくれる方、現場ドリブンにて現場の反応を直に感じながらのサービス開発志向のある方等、 EM/アプリエンジニア/SREと幅広く募集しておりますので、ご応募お待ちしております!

careers.kaminashi.jp

Go Conference 2021 Autumn に "Go"ld Partners として協賛・登壇します!

f:id:kaminashi-developer:20211110193200j:plain

カミナシではバックエンドのAPIサーバーの開発にGolangを利用しております。

(Goの採用に関しては過去のこちらの記事もご覧いただけると幸いです)

Go Conference に "Go"ld Partners として協賛させていただけることとなりました!

OSSの恩恵を受けプロダクト開発をさせていただいている弊社ですが、この度Go Conference 2021 Autumn に "Go"ld Partners として協賛させていただくこととなりました!

gocon.jp

gocon.jp

カミナシがGo Conferenceに協賛させていただくのは前回のSilver Partnersに引き続き2回目となります。 gocon.connpass.com

Sponsor Session で登壇させていただきます!

当日(11/13(土))は弊社エンジニアの木村が、10:30~ のSponsor Sessionで登壇させていただきます。

gocon.jp

デスクレスSaaSというあまり前例の無いサービスにおいてGoをどう利用しているのか、これまでの辛み・旨みを凝縮して共有させていただきますので、是非ご視聴いただければと思います。

開催日・参加申し込み

公開が直前となってしまい申し訳ございませんが、 Go Conferenceは今週末の2021/11/13(土) 10:00 〜 20:00の開催です!

開催はオンライン(YoutubeLive)で、チケット料金は無料。 参加申し込みは以下のconnpassページより可能ですので、ぜひお時間ある方はお申し込みください!

gocon.connpass.com

オフィスアワーにてブースも出展します

また、当日はメイン会場となるYoutubeLiveと並行して、Remoによるオフィスアワーにてブースも出展させていただきます!

ブースには弊社人事が常駐しており、当日参加するエンジニアメンバーも遊びに行きますので、少しでもご興味ある方はお気軽にお立ち寄りください!

カミナシに興味があるけど、都合が合わない場合・・

また当日都合が悪く参加できないという方も、下記採用情報サイトからご連絡いただけますとカジュアルにお話しすることも可能です。

こちらもご興味ある方はお気軽にご連絡ください!

careers.kaminashi.jp

最後になりますが、当日皆様にお越しいただけることを心よりお待ちしております!!

インフラ未経験エンジニアがGCPで爆速構築する奮闘記

こんにちは、株式会社カミナシのエンジニア @imu です。

はじめに

私はインフラに対して苦手意識(詳しく知らない)があり、issueに対して率先して改善しようとしませんでした。このまま深く触ることはないかなと思っていた矢先、新規プロダクトのインフラ構築をお願いされました。

めちゃくちゃ不安しかなかったのですが、最初から構築する機会なんて滅多にないのでチャレンジすることに!
(内心はやりたくないなと思ったけど…)

私自身は知らない領域にチャレンジすることは好きなので、楽しんでやろうと思いました。 苦手意識がある私がどのように、リリースできる環境を構築したか共有できればと思います!

注意)本内容はα版という位置づけで構築しており、正式リリース時には都度変更をしていきます。

Day 1(技術選定)

弊社はカミナシレポートというサービスを提供しています。このプロダクトはAWSを利用しており、その構成を真似て構築するの予定でしたが、インフラが苦手な私にとっては若干ハードルが高いです。ましてや一人でやるとなれば尚更ですよ…。

kaminashi.jp

となれば、イチから構築するなら別サービスを検討しても良いと思い、ちょうどその頃、社内でデータ基盤をGCP側に構築する話が上がっていたので、新規プロダクトはGCPに構築しようと思いました。
GCP経験者は社内にいませんでしたが、弊社のバリューに「β版マインド」があるのでチャレンジしたいと伝えて、運用が耐えれない場合は別のサービスに乗り換えるという話でGCPを採用しました。(と言うより、自分一人でやるならやってみたい方を採用したかったのが本音)

こういったチャレンジが弊社では出来るのでありがたいです。

Day 2(インフラ構成図)

とはいえ、いきなりGCPのコンソールをいきなり触るのはちょっとハードル高いです。 どういった機能が提供されているか分からないまま進められないのでインフラ構成図を書きました。構成図を書くことで頭の整理にもなるし、誰かに説明するとき楽ですね。

プロダクトに必要な機能から作成したインフラ構成図(α版)です! f:id:kaminashi-developer:20211025154736p:plain

インフラ全体はβ版以降まで見据えて書き出しましたが、正直これが正しいのか不安なんですよね…。

社内にGCPのインフラ経験者はいないため、業務委託で関わっていたメンバーに壁打ちをお願いしました!

予め構成図を共有してインフラMTGを開催し、気になる点やアドバイスをいただきました。

  • α版までの期間が短かったこともあり、作りすぎずに後から追加できるものは後回しにする
  • CI/CDを回せることを意識して、最小構成をを目指す

未経験あるある的なことだと思いますが、いきなり完璧なものを目指してしまったので小さく早く仕組みづくりをすることにしました。 (お恥ずかしながらここは私の反省点)

最終的にα版のインフラ構成図はこちらです! f:id:kaminashi-developer:20211025155152p:plain

Day 3-4(Cloud RunとFirebase HostingにDeploy)

Cloud RunにDeploy

基本は公式ドキュメント!
ドキュメントに従ってAPIを有効にしたり、cloudbuild.yamlを最小構成で作成しました。

未経験だといきなりは怖くて、以下の最小構成でまずDeployされるか確かめました。

  • Cloud BuildのTriggerを作成
  • featureブランチで回るように設定
  • ベタ書きで cloudbuild.yaml を作成

cloud.google.com

Githubの連携は共通アカウントを利用しましょう!個人アカウントだと予期せぬ問題になりますからね

導線が出来た後はBuildやCloud RunへのDeployを cloudbuild.yaml に追加してDeployできました。 (APIを有効にしたり権限周りで出来ないことはありましたが…)

FirebaseにDeploy

WebアプリケーションのDeployはFirebaseを利用して、公式ドキュメントを見ました。

cloud.google.com

余裕余裕!と思っていた矢先、Firebase側からプロジェクトが見えないという問題に当たりました。

f:id:kaminashi-developer:20211102180211p:plain

ドロップダウンリストに追加したプロジェクト名が表示されない!別のプロジェクトは表示されているのに…。

ドキュメント等を探しましたが検討が付かず、他のプロジェクトの差分を見ると怪しい箇所がありました。

結論はGCPで新規プロジェクトを作成するとき、プロジェクト名とプロジェクトIDを同一にしていると表示されないです。

f:id:kaminashi-developer:20211102180536p:plain

一度プロジェクトを作成するとプロジェクトIDは編集できないため、新規プロジェクトで kaminashi-blog-123とすれば表示されました。

原因が分かったので新しく作り直すことに…。

Day 5(Cloud SQL for MySQLインスタンス作成)

インスタンスを作成するだけなので、公式ドキュメントを見て最小構成で作成しました。

cloud.google.com

α版のため基本最小構成でインスタンスを作成しています。 f:id:kaminashi-developer:20211025222626p:plain CPUやメモリ等は利用者数の増加に伴って、随時変更をしていきます!

GolangでCloud SQL for MySQLに接続するとき、下記ドキュメントを参考にしましたが、接続文字列が //cloudsql になるので注意してください。 f:id:kaminashi-developer:20211026160929p:plain

cloud.google.com

あとはCloud Buildを再実行してDB接続まで出来ていることを確認しておわりです。

Day 6(Cloud Runのカスタムドメイン設定)

カミナシレポートでAWS Route53を利用していたのでこちらを使います。

公式ドキュメントを見れば困ることはなかったです! cloud.google.com

f:id:kaminashi-developer:20211102181739p:plain HTTPS 接続用のマネージド証明書が自動的に発行、更新されるので便利ですね。こういった細かいことをGCP側がやってくれるので非常に助かってます。
ちなみにマッピングが終わるまでに40分くらい掛かりました。

詰まったポイント

  • ググったときに公式ドキュメントじゃなくて個人ブログを参考にすると、内容が古い場合が多かったので公式ドキュメントを見よう!
    • GCPも日々進化しているので公式ドキュメントが一番良いなと思います。可能であれば現役でGCPを触っているエンジニアと壁打ちできると尚良いですね。
  • GCPAPIサービスの有効化、サービスアカウントなどに権限付与は、Try&Errorを繰り返すしかない。
    • Cloud LoggingでCloud Buildのエラーを見るとわかりやすいが、一覧で分からないのが難点。

おわりに

インフラ未経験の私が爆速でインフラ構築出来たのも、業務委託のメンバーと週次で壁打ちや、随時困ったときに相談出来たことが要因だと思います。苦手な私に対してペアプロをしていただいたり、様々な提案をしていただきホントに助かりました。

まずは手動で動く環境を構築するのを優先したため、今ではTerraform化や別の環境を作ったり、私自身がインフラに対する意識も変わりました。 あれこれやってみたい、これはどうすればいいんだろう?などの課題を上げ、解決していくのが楽しくなりました。

とはいえ、まだ正式リリースまでにやることはあります!リリース後も都度改善をしていく必要があるのでチャレンジしがいがありますよ。

また、今のフェーズは領域に縛られない働き方が出来て、カミナシではやってみたいことや実現してみたいことなどが叶いやすい環境だと思っています!

「お、カミナシ気になるなー」と思った方はカジュアルにお話できればなと思いますので、TwitterのDMや下記の採用情報サイトからご連絡お待ちしております!

最後まで読んでいただき、ありがとうございました! careers.kaminashi.jp

【GORM】V1からV2へアップグレードにチャレンジした話

f:id:kaminashi-developer:20210803192112j:plain こんにちは、株式会社カミナシのエンジニア @imu です。

はじめに

弊社のアプリケーションのバックエンドはGoでDBアクセスライブラリはGORMを使っています。
サービスリリース時からGORMを使っており、V1からV2にすることでパフォーマンス改善につながると思い、アップグレードにチャレンジしました。
色々詰まるところがあったので、共有を含めて何をやったのか書いてみます!

結論

早速結論ですが…
GORM V2へのアップグレードは現時点で保留となりました...
保留にした理由は以下の通りです。

  • V2で仕様変更になった関数が多く影響範囲が広い
  • V2で廃止された関数があり代替案の検証が必要である
  • そもそもAPIのテストが完全でないのでアップグレードが怖い

APIの網羅テストがあれば思い切った変更ができたのですが、完全ではないので一旦網羅テストを書いてからアップグレードすべきという結論に至りました。

ここから先はどういった変更対応をしたのかを共有したいと思います。
一部未確認の箇所がありますのでご了承ください。

対応内容

Logger, Datadog Trace, サードパーティ製のBulkInsertも変更しておりますが、あまり記事で見かけない対応を共有したいと思います。

  • PrimaryKeyの指定方法を変更
// V1
`gorm:"primary_key" json:"id"`

// V2
`gorm:"primaryKey" json:"id"`
  • jointable_foreignkey, association_jointable_foreignkeyの指定方法を変更
// V1
`gorm:"jointable_foreignkey:hoge;association_jointable_foreignkey:huga;" json:"ids"`

// V2
`gorm:"joinForeignKey:hoge;joinReferences:huga;" json:"ids"`
  • foreignkey, association_foreignkeyの指定方法を変更
// V1
`gorm:"foreignkey:hoge;association_foreignkey:huga;" json:"id"`

// V2
`gorm:"foreignKey:hoge;references:huga;" json:"id"`

詳細はこちらを確認してください。
gorm.io

  • V2でRecordNotFound()判定

エラーハンドリング方法が変わっているため、自前で共通関数を作成し、errorオブジェクトを渡して判定するようにしました。

import (
    "errors"

    "gorm.io/gorm"
)

func RecordNotFound(err error) bool {
    return errors.Is(err, gorm.ErrRecordNotFound)
}

gorm.io

  • First, Findのエラーハンドリング

RecordNotFound()メソッドがV2から廃止になったことで、エラーハンドリングで注意が必要なケースがありました。

// V2
var db *gorm.DB
user := &orm.User{}
...
err := db.Where("uuid = ?", library.UUID()).First(user).Error
pretty.Println(err)
pretty.Println(errors.Is(err, gorm.ErrRecordNotFound))

> &errors.errorString{s:"record not found"}
> bool(true)

user := &orm.User{}
err := db.Where("uuid = ?", library.UUID()).Find(user).Error
pretty.Println(err)
pretty.Println(errors.Is(err, gorm.ErrRecordNotFound))

> nil
> bool(false)

.Find().Errorとした場合、gorm.ErrRecordNotFoundにならないので注意してください。
.First().Errorにメソッドを変更するか、nilならNotFoundにするようにしましょう。

  • テーブル名取得
// V1
var db *gorm.DB
var st orm.Users
...  
pretty.Println(db.NewScope(st).TableName())

> users

// V2
var db *gorm.DB
var st orm.Users
...
stmt := &gorm.Statement{DB: db}
stmt.Model = st
stmt.Parse(stmt.Model)
pretty.Println(stmt.Table)

> users

V2でテーブル名を取得する処理は、Execute()時点で何かしらしていると踏んでコードを読んで書きました。 f:id:kaminashi-developer:20210802190729p:plain

  • カラムチェック
// V1
var db *gorm.DB
var st orm.Users
...  
pretty.Println(db.NewScope(st).HasColumn("Hoge"))

// orm.UsersにHogeカラムがあればtrue
> bool(true)

// V2
var db *gorm.DB
var st orm.Users
...  
pretty.Println(db.Migrator().HasColumn(st, "Hoge"))

> bool(true)
  • QueryExpr
// V1
var db *gorm.DB
var st orm.Users
...  
pretty.Println(db.Table("sample").Where("id = 1").QueryExpr())

> &gorm.SqlExpr{
    expr: "SELECT * FROM `sample`  WHERE (id = 1)",
    args: nil,
}

// V2
var db *gorm.DB
var st orm.Users
...  
pretty.Println(db.Table("sample").Where("id = 1"))

> V1の処理はサブクエリだと思っているのでコレで大丈夫なはず…(未確認です)

おわりに

個人的にはやりきりたいと思って取り組みましたが、思いの外時間が掛かりすぎるのと、V2に変更したときの安心感(テスト)がないので保留という着地をしました…。
限られたリソース内で出来なかった悔しさを晴らすために(?)、弊社では一緒に働くメンバーを幅広く募集しております!

特に…

  • 一つの課題に向き合うことが好きな方
  • 難易度が高い課題が好きな方
  • 作ったモノがどういった使われ方をしているのか興味があり、現場に行きたい方

です!

興味がある、話を聞いてみたい、応募したいという方はお気軽にご応募ください!

herp.careers

PMの自分が如何にユーザーの声をエンジニアの共通言語へ変えたか

f:id:kaminashi-developer:20210707104815p:plain 初めまして。株式会社カミナシでPMをやっているGTOです。
先日6/30に、マネーフォワードさんと合同で勉強会を実施しました!当日お集まりいただいた皆様、ありがとうございます。
登壇者として出席させていただき、「PMの自分が如何にユーザーの声をエンジニアの共通言語へ変えたか」というタイトルで発表しました。

カミナシでも、紙というツールを介して長年使われ続けたそのフローをいかにプロダクトで解決していくのかは大きなテーマの一つです。
そんな中でユーザーさんへの価値をどう作り、またプロダクトチームへどう伝えていったのかを話してきました。

当日の登壇でお話し出来なかった部分もあるので、一部掘り下げてこの記事内で紹介します!

登壇資料

登壇の概要

お客さんへの価値をどのように見つけ、チームと対話をし伝えるのかは最近の自分の悩みごとでした。

  • お客さんへの価値をどのように見つけ => 仮説検証
  • チームと対話 => チーム間のコミュニケーション

「使われる」プロダクトを「最小単位」でどうやって作ったらいいんだろうね?と模索の過程を話しています。

仮説のアイデア出し

仮説を作る前にまず、デザインスプリントを使いアイデアを集約させます。
MTGを毎回行うより、討論しながら一緒に知識の獲得とアイデア出しを行えるメリットがあります。

www.gv.com

通常デザインスプリントは1週間をかけてサイクルを回し切ることが多いのですが、1日で背景揃える/課題の整理/アイデア出しをやっています。
ビジネス含め、全員が揃うタイミングもなかなか合わせづらい部分もあるのでギュッと凝縮してやります。

ここはいろんな背景のメンバーを少人数集めるのが重要かなと思っていてます。後、アイデア出しなので楽しくやるのも結構大事。

f:id:kaminashi-developer:20210706085734p:plain
妄想設定作りながら、ワイワイやってる

実際に背景を揃える部分では、動画のイメージやソリューション, 戦略を共有したり、人物像が複雑に入り組んでいるものは登場人物の関係図を整理したりします。

f:id:kaminashi-developer:20210704135413p:plain
動画のイメージやソリューション, 戦略を共有
f:id:kaminashi-developer:20210704135752p:plain
登場人物の関係図を整理

手法自体は、デザインチームが主導でファシリをしたりしますが、ビジネスとプロダクトをうまく役割分けて準備をします。
それぞれの知識や手段に偏りがあればあるほど、議論としては極端に傾きいい意見が出て良いです。

f:id:kaminashi-developer:20210707105121j:plain
「そもそもこのお客さんのフローをなくしてしまう」、「AIで紙無くす」、「ハサミで切るように関係性を断ち切るUI」などが上がった

実現可否は一旦横に置き、アイデアを広げる方が思いつかなかった発想につながって良い議論が出来るのでオススメです。
出したアイデアを元に、仮説をテーブル上に並べて取り組むべき課題を選択し、前に進めていきます。

身銭を切る仮説を作る

f:id:kaminashi-developer:20210703160744p:plain

仮説を選ぶ時に意識するのは、「そのアイデアに身銭を払ってくれる」仮説かどうかだと思います。

www.amazon.co.jp

「身銭」は、本気で取り組むというあかしになり、必要な準備が整い、困難があっても投げ出さないという宣言にもなる
重要なのは相手の意見や予想ではない。あなたの製品に対し、ターゲットとなる人たちが切ってくれる「身銭」なのだ!

Google×スタンフォード NO FLOP! 失敗できない人の失敗しない技術』 アルベルト・サヴォイア 著 石井 ひろみ 訳 サンマーク出版

身銭というものは、お互いの本気で取り組むための制約です。
作り手と受け取り手のどちらもが目の色を変えていく必要があり、それは身銭によってより良い緊張感が生まれます。

なのでプロダクトを作りきる前に、アイデアや仮説から作ったプロトタイプを提げて売りに出します。
ここはデザイナーの力を借りて、アイデアを形にしていきます。

f:id:kaminashi-developer:20210706084037p:plain
デザイナーとプロトタイプ作っていく

実際に売ってみると、仮説自体の筋が良かったのかの判断がつきやすくなります。
売る前は良さそうだなと思ったものも、商談にいざ出してみると全くだめなこともあったりします。
外部に評価をされる中で大きく外し、次の手を高速で考え出しまた次に活かすこと。
失敗をすることも、学びに繋がることもたくさんあるので、ここは執着して当てるまで作りこみます。

f:id:kaminashi-developer:20210704133017p:plain
ダブルダイヤモンド的に思考を交差させる

仮説作って事実を集めて失敗し、また次のより良い仮説を作る。
発散と収束を繰り返し思考を交差させながら売りに行き、次の仮説を作っていくのが良いかなと思います。

体験をチームへ共有する

イデアや仮説や作るものも徐々に決まり、チームへ伝えていきます。
お客さんの体験をチームへどう伝えるのかの部分は、見て感じたお客さんの行動を元に整理していきます。

f:id:kaminashi-developer:20210704152508j:plain
行動を整理する
goodpatch.com

僕らはKA法に近い手法でユーザーの行動や感情から、価値を抽出し価値マップを作成します。
ユーザーで伝えたいような体験となるものになるので、設計/関係性/背景を残すようにします。

f:id:kaminashi-developer:20210704153153p:plain
MVP

機能単位では、ユーザーストーリーマッピングをしていきながら、Minimum Viable Product(最初のリリースで達成したいこと)では何を作るべきなのかを残しています。
MVPで意識するべきことは、最小の機能で、価値を最大のに出来る体験はどんなものかを定義するだと思います。

medium.com

いかに最小の労力で、一番初めのお客さんに愛してもらえるような体験をどう作るのかが観点で、単一の機能を作ることだと曲解しないようにする必要があります。
ここが誤ってしまうと、お客さんに本来提供したい運用フローが機能不足で本来提供したい価値が伝わりきらないことがあります。
それは正しい検証が出来たとは言えません。
機能ではなく本来提供したい体験や価値を実現できる最小限の機能なので、変に削りすぎないことは割と大事な観点だったりします。

まとめ

仮説検証, チーム間のコミュニケーションを正しく行うためにも、チームが正しくお客さんの価値へ向き合うことが重要だよね!ということを登壇の中で伝えてきました。

開発もなるべく作ったものがお客さんに使われて喜ばれる姿を一緒にワイワイ共有したいし、失敗した時には一緒になんで失敗したのかを真剣に振り返っていきたい。
スタートアップで僕らもまだまだ未知なことも多いし、失敗も多く重ねていきます。
そんな失敗をチームが修正できる仕組みが最高ですし、そんな強い組織を作りたい気持ちです。
仕組みを徐々に整えることで、少しでもチームが自律的にお客さんへ向かうことが重要だな、もっとやれることもあるなと振り返る登壇となりました。
事業の成長に向けてより一層引き締めていきたい所存です!

最後に宣伝です。

カミナシでは仮説検証を始めとして、他にも事業が前にすすむような技術的にも事業的にもチャレンジが出来る企業です。
幅広く全職種で募集しています。エントリーお待ちしております!!

herp.careers

では👋

GolangでのAPI GatewayとLambdaを利用した認可設定をやってみる

f:id:kaminashi-developer:20210625112557j:plain

こんにちは。エンジニアの @Taku です。

期間が空いてしまいましたが、過去にLambda オーソライザーを使った認証を作成した続きで、今回は認可の設定を行っていきたいと思います。

※本記事の内容はプロダクトと関係なく、個人的にやってみたものになります。

前回までにやったこと

前回までで、

API GatewayとLambdaオーソライザーを利用した認証の作成 kaminashi-developer.hatenablog.jp

をやった後、

② ①の認証をAWS Samを用いて構築 kaminashi-developer.hatenablog.jp をやってみました。

よろしければ上記もご参照いただけますと幸いです。

今回やること

今回は認可の設定として、ログイン後のリソースのアクセス制御を行っていきます。

認可の設計パターンにつきましては、以下記事を参考にさせていただきました。 please-sleep.cou929.nu

これまでの実装のパターンとしては、上記記事でのJWT 利用 + API Gateway パターンが近いかと思いまして、当てはめると - TokenStore = Firebase Auth - AuthService = Lambda Authorizer関数 - Service X = SAMで定義したLambda関数 になるかと思います。

認可の設計パターンについてはいくつか考えられますが例として、

  1. サービス側で権限を制御するパターン
  2. 中央管理するパターン

があり、これがベストという方式はないようなのですが、今回はAPI GatewayとLambdaオーソライザーを利用するため、2の中央管理するパターンで実装していきます。

認可の設定

アクセス制御のパターンは、以下3ユーザーで

  1. 管理者(admin) :全リソースにアクセスできる
  2. リーダー(leader):リーダー及び、他のユーザーのリソースにアクセスできる
  3. ユーザー(user) :自身の情報にのみアクセスできる

といった形で制御したいと思います。

API GatewayとLambda オーソライザーを利用した認可の方法

以後、過去からの続きで恐縮ですが、サンプルを元にGolangでLambdaオーソライザーの関数を作成した場合、 IAM policyを作成する箇所でユーザー毎にアクセスできるリソース(パス)を制御することができます。

具体的には events.APIGatewayCustomAuthorizerResponseを作成している箇所で、 events.APIGatewayCustomAuthorizerPolicyResource で、ポリシーを付与されたユーザーはここに記載されているパスにのみアクセスすることができます。

// generatePolicy IAM policyを生成する
func generatePolicy(principalId, effect string, resources []string) events.APIGatewayCustomAuthorizerResponse {
    authResponse := events.APIGatewayCustomAuthorizerResponse{PrincipalID: principalId}

    if effect != "" && len(resources) > 0 {
        authResponse.PolicyDocument = events.APIGatewayCustomAuthorizerPolicy{
            Version: "2012-10-17",
            Statement: []events.IAMPolicyStatement{
                {
                    Action:   []string{"execute-api:Invoke"},
                    Effect:   effect,
                    Resource: resources, // アクセスOKなパス
                },
            },
        }
    }
        return authResponse
}

ですので、 generatePolicyを利用してポリシーを作成する際、各権限別にアクセス可能なパスを設定することでアクセス可能なリソースを制御することができます。

実装

続いて具体的な設定です。

API Gatewayの設定

本題から外れるため詳細は省く&設定も極小にしておりますが、前回のSAMのtemplate.yamlを修正し、

  • GET: /admin (管理者のページ)
  • GET: /leaders (リーダーのページ)
  • GET: /users/XXXXXXXX (一般ユーザー個人のページ)

というリソースを追加しました。

前回のAWA SAMを使った構築で作成した template.yamlを以下のように修正します。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description:  Sample API Gateway Lambda Authorizer by Golang.
Resources:
  SampleRestApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: dev
      Description: "This is Sample Rest API"
      Cors:
        AllowMethods: "'GET, OPTIONS'"
        AllowHeaders: "'*'"
        AllowOrigin: "'*'"
      Auth:
        DefaultAuthorizer: MyLambdaAuthorizerFunction
        Authorizers:
          MyLambdaAuthorizerFunction:
            FunctionArn: !GetAtt LambdaAuthorizerFunction.Arn
  RootFunction:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /
            Method: get
            RestApiId:
              Ref: SampleRestApi
      Runtime: python3.7
      Handler: index.handler
      InlineCode: |
        def handler(event, context):
          return {
            'statusCode': 200,
          }
  # 追加ここから
  AdminFunction:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /admin
            Method: get
            RestApiId:
              Ref: SampleRestApi
      Runtime: python3.7
      Handler: index.handler
      InlineCode: |
        import json
        def handler(event, context):
          return {
            'statusCode': 200,
            'body': json.dumps({
              'message': 'admin情報',
            }),
          }
  LeaderFunction:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /leaders
            Method: get
            RestApiId:
              Ref: SampleRestApi
      Runtime: python3.7
      Handler: index.handler
      InlineCode: |
        import json
        def handler(event, context):
            return {
            'statusCode': 200,
            'body': json.dumps({
              'message': 'leader情報',
            }),
          }
  UserFunction:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /users/XXXXXXXX # userのuid 
            Method: get
            RestApiId:
              Ref: SampleRestApi
      Runtime: python3.7
      Handler: index.handler
      InlineCode: |
        import json
        def handler(event, context):
            return {
            'statusCode': 200,
            'body': json.dumps({
              'message': 'user情報',
            }),
          }
  # 追加ここまで
  LambdaAuthorizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: samAuth
      CodeUri: ./lambda_auth # buildされるが、configがアップされなかったので、`/build/関数名`のディレクトリに手動で配置
      Handler: app.lambda_handler
      Runtime: go1.x

Outputs:
  SampleRestApi:
    Description: "Sample API Gateway Lambda Authorizer by Golang."
    Value: !Sub "https://${SampleRestApi}.execute-api.${AWS::Region}.amazonaws.com/dev/"

Lambdaオーソライザー関数の設定

前々回のLambda fanctionを修正し、generetePolicyを呼んでいた箇所でリソースの設定を行います。

以前はgeneretePolicyの第3引数にevent.MethodArnを入れており、Lambdaオーソライザー関数を呼んだ際のパスが入るようになっていたため認証後はそのままアクセスできていたのですが、今回はユーザー単位でアクセスできるパスを設定します。

apiArn := "arn:aws:execute-api:ap-northeast-1:XXXXXXXXXXXX:XXXXXXXXXX/dev" // API Gatewayのパス

    switch email {
    case "admin@example.com":
        return generatePolicy("admin", "Allow", []string{apiArn + "/GET/*"}), nil // 全リソースにアクセス可能
    case "leader@example.com":
        return generatePolicy("leader", "Allow", []string{apiArn + "/GET/leaders", apiArn + "/GET/leaders*", apiArn + "/GET/users*"}), nil // leaderとuserの全リソースにアクセス可能
    case "user@example.com":
        userID := token.UID
        return generatePolicy("user", "Allow", []string{apiArn + "/GET/users/" + userID}), nil // 自分のリソースにのみアクセス可能
    case "deny@example.com":
        return generatePolicy("deny", "Deny", []string{}), nil
    case "unauthorized":
        return events.APIGatewayCustomAuthorizerResponse{}, errors.New("Unauthorized") // Return a 401 Unauthorized response
    default:
                 return events.APIGatewayCustomAuthorizerResponse{}, errors.New("Error: Invalid token")
    }

リソースに設定するパスは、コンソールでAPI Gatewayの設定を見ることで確認可能です。 f:id:kaminashi-developer:20210624212136p:plain

generetePolicyの第3引数はSliceで、複数パスを入れることができ、 *を入力して配下の全リソースにアクセス可能とすることもできます。

が、 leaders/*のように /を挟むと別リソースと判定されるようで、 leaders*とする必要があったためパス設計には注意が必要そうでした。

今回はメールアドレスでの判定ですが、Firebaseでアカウントを作成する際に情報を組み込むようにすれば、それらを基に制御をすることも可能かと思います。 (コンソールからだとカスタマイズができなかったため割愛)

検証

FirebaseAuthで以下の3ユーザーを作成し、アクセスを試みます

検証にはこれまた雑ですが、ログイン後、各API Gatewayのパスにリクエストを投げ、どのユーザーでどのレスポンスが返ってくるかHTMLファイルを利用して確認します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Lambda Authorizer Sample</title>
  </head>
  <body>
    <h1>...Please wait</h1>
    <div id="email"></div>
    <div id="token"></div>
    <br>
    <div></div>
    <a id="admin"></a>
    <br>
    <div></div>
    <a id="leader"></a>
    <br>
    <div></div>
    <a id="users"></a>

    <!-- FirebaseSDKのインポート -->
    <script src="https://www.gstatic.com/firebasejs/8.2.6/firebase-app.js"></script>

    <!-- FirebaseAuthのインポート -->
    <script src="https://www.gstatic.com/firebasejs/8.2.6/firebase-auth.js"></script>

    <!-- configファイルのインポート -->
    <script src="./js/config.js"></script>

    <!-- axiosのインポート -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js" integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ==" crossorigin="anonymous"></script>
    <script>
      // User情報の表示
      firebase.auth().onAuthStateChanged(function(user) {
        let h1    = document.querySelector('h1');
        let email = document.querySelector('#email');
        let token = document.querySelector('#token');
        let admin  = document.querySelector('#admin');
        let leader  = document.querySelector('#leader');
        let users  = document.querySelector('#users');

        // API Gatewayで作成したエンドポイントを設定
        // const url = "http://localhost:3000"
        const adminUrl  = "https://x7ow8vkh09.execute-api.ap-northeast-1.amazonaws.com/dev/admin"
        const leaderUrl = "https://x7ow8vkh09.execute-api.ap-northeast-1.amazonaws.com/dev/leaders"
        const userUrl   = "https://x7ow8vkh09.execute-api.ap-northeast-1.amazonaws.com/dev/users"

        if (user) {
          console.log('user', user)
          h1.innerText   = 'ログインしました';
          email.innerHTML = `メールアドレス:${user.email}`;
          user.getIdToken().then(function(accessToken) {
            console.log('accessToken', accessToken)
            token.innerHTML = `JWTトークン:${accessToken}`;

            // リクエスト送信
            axios.get(adminUrl, {
              headers: {
                'Authorization':accessToken,
              }
            }).then(res => {
              admin.innerHTML = `管理者: ${res.data.message}`;
            }).catch(error => {
              admin.innerHTML = `管理者: ${error}`;
            });

            axios.get(leaderUrl, {
              headers: {
                'Authorization':accessToken,
              }
            }).then(res => {
              leader.innerHTML = `リーダー: ${res.data.message}`;
            }).catch(error => {
              leader.innerHTML = `リーダー: ${error}`;
            });

            axios.get(userUrl + "/" + "8eMzOlFSDcVJU8U4zjwNA9pyMlr1", {
              headers: {
                'Authorization':accessToken,
              }
            }).then(res => {
              users.innerHTML = `ユーザー: ${res.data.message}`;
            }).catch(error => {
              users.innerHTML = `ユーザー: ${error}`;
            });
          }, null, ' ');
        }
      });
    </script>
  </body>
</html>

各ユーザーでログインしたところ、以下の通りアクセス可能となっているリソースのみ取得できている形になってます。

  • admin@example.com

    • 全情報が取得できている f:id:kaminashi-developer:20210624212203p:plain
  • leader@example.com

    • leader, user情報のみ取得できている f:id:kaminashi-developer:20210624212430p:plain
  • user@example.com

    • user情報のみ取得できている f:id:kaminashi-developer:20210624212408p:plain

終わりに

以上、簡単ではありますがAPI GatewayとLambdaを利用した認可設定のご紹介でした。 URLの設計が必要ですが、API GatewayとLambdaを利用することで、中央管理するパターンであれば簡単にロールベースのアクセス制御ができるように感じました。

少しでも参考になれば幸いです。

【登壇資料】製造現場を変えろ。ソフトウェアエンジニアが挑む爆速DX

f:id:kaminashi-developer:20210603133800p:plain

カミナシ・エンジニアの浦岡です。 先日、CADDiさんと共催で勉強会を開催しました。その際のカミナシ側の登壇資料を紹介します。

勉強会の概要

製造現場向けのサービスを開発しているCADDiさんと弊社カミナシが共催でオンライン勉強会を開催しました。

今回、製造現場向けのサービスを作る2社が、 ソフトウェアエンジニアが挑む爆速DXと題して開発エピソードをそれぞれ発表しました。

kaminashi.connpass.com

カミナシからは「紙のワークフローをDX化する際に行った多階層データ処理の高速化について」というタイトルで発表させていただきました。

speakerdeck.com

製造現場のDXとは

gyazo.com

製造現場のDXというと、無人化、高度化をイメージする方もいるかと思いますが、実際の製造現場ではまだまだ紙が主役です。

製造現場を変えるための、カミナシのアプローチ

gyazo.com

カミナシは、紙のワークフローをデジタル化することで製造現場のDXを加速しようしていますが、 ユーザー自身がノーコードでワークフローを作成・編集できます。

開発エピソード(性能低下の課題)

ノーコード(ロジカル)なデータを扱うことに起因した性能低下の課題が サーバー、クライアントそれぞれで発生し、それにどう対応したかを紹介せさていただきました。

サーバー側での性能低下

gyazo.com

サーバー側では、ボトルネックとなっていたDBクエリの発行回数を抑える施策を紹介しました。

クライアント側での性能低下

gyazo.com

クライアント側では、コンポーネントの無駄な描画による性能の低下に対して、memo化で対処した事例を紹介しました。

宣伝

今回、サーバー・クライアントのそれぞれでの性能低下の課題に対しての対処を開発エピソードとして発表しましたが、まだまだ小手先の対応しかできていないのが実情です。

今後のユーザー増加、機能拡張に耐えうる抜本的な改善を行うためにも、 EM/アプリエンジニア/SREと幅広く募集しています!

open.talentio.com

open.talentio.com

open.talentio.com