Amazon SNSでiOS Push通知を証明書ではなく認証キーで認証する

はじめに

こんにちは、技術開発部 事業横断チーム データエンジニアの多田です。
入社時はアプリエンジニアだった私、hinataアプリの保守・運用をたまにやっております。
最近、Amazon SNSiOS Push通知認証方法を証明書形式から認証キー形式に変更しました。
ただ、Amazon SNSでの認証キーを使ったPush通知認証についてネットにあまり情報がなかったため、記事にまとめてみます。

概要

hinataアプリのPush通知にはAmazon SNSを使用しています。

最近、Amazon SNSに登録しているiOS Push通知証明書を更新する必要がありました。
作業する際に、2021年11月にAmazon SNSでも認証キーに使用ができるようになった事に気づきました。 これを機に証明書による認証から更新が不要な認証キーによる認証に変更する事としました。(参考

本記事ではその変更作業の内容及びトラブルシューティングを記載します。

Push通知の証明書?認証キー?

iOSアプリでPush通知を利用する場合、Apple Push Notification Service(以下、APNs)にリクエストを送ります。
この時、Appleに対して認証が必要で方法は下記2つあります。

  • 発行から1年の有効期限があるSSL証明書
  • 有効期限がない認証キー

証明書の方が古いやり方で、現在は認証キーの方をAppleは推奨しているそうです。
ちなみに、証明書の方は環境毎(product, staging, sandbox等々)に発行する必要がありました。
ですが、認証キーはベンダー共有になるため1個発行すれば使い回せます。

詳細は下記を。とてもわかり易かったです。
【iOS】Firebase Cloud Messagingで利用するAPNs認証キー・証明書の作り方 - Qiita

変更作業

Amazon SNSに認証キーを登録する

下記、設定を間違えると全デバイスにPush通知が届かなくなる恐れがあるため、慎重に。

手順

  1. APNs認証キーをAppleDeveloperから発行する(手順に関しては上記Qiitaのリンク参照)
  2. Amazon SNSの管理画面に入る
  3. 左サイドバーMobileプッシュ通知
  4. プラットフォームアプリケーションにある該当アプリケーションを選択
  5. 編集を押して、下記の通り設定
    1. 認証方法: トークン
    2. 署名キー: 1.でダウンロードした.p8ファイルをアップロード
    3. 署名キーID: Certificates, Identifiers & Profiles - Keysで、1で作成したキーを開いて出てくる Key ID
    4. チーム ID: Apple デベロッパーアカウントのMembershipページにある
    5. バンドルID: Certificates, Identifiers & Profiles - Identifierにある該当のアプリを開いて出てくるBundle ID
  6. 入力し終えたら 変更の保存
  7. Push通知を送ってみて届いたら完了。なお、全エンドポイントへのPush通知をしてしまうと、仮に設定が間違っていた場合全てのエンドポイントが無効になってしまう。個別Pushで試す事を推奨。

トラブルシューティング

テストPush通知後、全てのエンドポイントが無効になってしまった

最初、上記手順5で間違ったバンドルIDを登録した後、手順7で全エンドポイントにPush通知を送ってしまいました。
すると、Amazon SNSに登録されたエンドポイントが全て無効になってしまいました…
無効になるとどうなるかというと、認証情報を正しく設定し直したとしてもPush通知がそのエンドポイントへ届かなくなります。

この時はsandbox環境で行っていたため、影響は少なく済みました…本番環境でと思うとゾッとしますね。

なぜ無効になってしまったかというと、無効な認証情報に対してPush通知を行おうとしたためです。
下記、Amazon SNS プッシュ通知のエンドポイント編集画面に記載があるように、無効なPush通知リクエストがあるとAmazon SNSがエンドポイントを自動で無効にしてしまうようです。

エンドポイントへの配信を有効にします。エンドポイントが無効であることを通知サービスが Amazon SNS に示すと、Amazon SNS はこれを false に設定します。

無効になってしまったエンドポイントは、一応エンドポイント編集画面で有効・無効は切り替えられます。
かつ、APIも用意されているため↓、一括で無効→有効に切り替えることも出来ます。
SetEndpointAttributes

とあるSREの自宅PC事情

こんにちは!技術開発部マネージャーをしている井島です。 今回はvivit に私が入社当時(2020年1月)に開発部で社内LTをやっていたことがあり、趣味全開の内容があったので、それを紹介しようと思います!

vivitでは2021年8月からマネージャーを行っていますが、それ以前はSREでした。前職では新卒からインフラエンジニアをやっており、この分野は好きです。

なんだかんだ20万円以上かかっている

まずは全体の写真です。

f:id:ijimakenta:20220410183120p:plain f:id:ijimakenta:20220410193610p:plain

ブログ執筆現在もこの構成なのですが、高スペックPCが買えるくらいですね...

ベアボーンという小さな筐体を使ってます。バッファローのHubはジャンボフレーム対応。電源タップは業務用でも使うサンワサプライ製で雷ガード付き。
ちょっとしたこだわりポイントです。

いわゆるハイパーコンバージドインフラ

f:id:ijimakenta:20220410183448p:plain そもそも、「自分のPCにスケールアウトできるストレージが欲しい」というところから始まりました。

非機能要件はこんな感じです。

  • ハードウェアは交換可能で、一般家庭用向けのものを使用する。
  • ストレージシステムのデータ部分に関してシングルスポットをつくらない。
  • 使わない時はシステムを停止する。
  • HDDレベルのシーケンス読み込み、書き込み性能。
  • ストレージシステム障害時、バックアップデータへは復旧作業なしにアクセス可能。
  • Windows, Linux, Mac, Android の各デバイスからストレージへアクセス可能。

SDS (Software Defined Storage) を使用

ceph.io ceph というミドルウェアを使いました。構築当時、SDS分野が流行っていて、OSSだとこれが一番利用実績があったので選びました。

普通だったらRAIDがまっさきにに思い浮かぶと思いますが、RAIDカードが故障した場合、交換可能かどうか不安があり却下しています。
例えばRAIDカードが故障したときに同じ型のものを調達できるのか?違うRAIDカードに交換したとき、正しくディスクが認識されるのか?など、、、

kubernetes について

ストレージシステム技術スタックの写真で cephの上にk8s があったと思います。これはceph サーバのメトリクスを取得するためと、samba サーバ起動するためです。(これだけのためです笑)

これも当時ちょうど流行っていたってことも加えて、これを使えば prometheus, grafana, samba 起動サーバを cephノード増減に左右されないと見込んで採用しました。

k8s クラスタのマスターノードにについては、使わない時クラスタを停止することを考慮し、クラスタ起因の障害を回避するためにシングル構成にしています。

ディスクトップPCスペック

いつも使うPCです。普通ですよね!?
CPUファンはこだわって、静音、ベアボーン筐体に入るものを探しました。

  • Desk Mini 110/B/BB
  • Core i5 7500 BOX
  • CFD D4N2133PS-8G [SODIMM DDR4 PC4-17000 8GB]
  • NH-L9i(Noctua) ※CPUファン
  • 変換名人のPCIB-USB2/2FL
  • Debian testing

サーバ側スペック

サイジングについて話し出すと長くなりすぎるので、ここではふ〜んくらいでお願いします。

  • Desk Mini 110/B/BB
  • Intel CPU Celeron G4900 BOX
  • crucial CT8G4SFS824A [SODIMM DDR4 PC4-19200 8GB]
  • NVMe タイプ 2280 PCIe Gen3 x4
  • SEAGATE ST1000LM049 [1TB 7mm] 7200rpm
  • SEAGATE ST1000LM049 [1TB 7mm] 7200rpm
  • CentOS 7

最後に

細かい設計思想についても話したいのですが、長くなりすぎるのでここでは紹介だけにとどめておこうと思います。 vivit では中途エンジニアも積極募集中です。面談や面接でお会いしたときなど、興味があれば是非質問して下さい!

www.wantedly.com www.wantedly.com

ちなみに、cephをバージョンアップした時、1回失敗してデータ飛んでいます...
もちろんバックアップから復旧しましたよ。

23卒エンジニアのvivit体験記

こんにちは!技術開発部の北條です。 私は23卒エンジニアとして入社が決まっており、2022年3月からインターンとして参加しています。

今回はvivitへ入社を決めた理由や一ヶ月間のインターン内容、vivitの雰囲気について紹介します。

本ブログでは、21卒の氏家さんが入社した理由やインターンの体験談についての記事も公開されています。私自身、就職活動において「年齢の近い新卒の言葉」がとても参考になったので、この記事が24卒以降の方や技術開発部に興味のある方の参考になれば幸いです。

vivitの内定を決めた理由

私はWantedlyでvivitのミートアップに参加したのが初めての出会いでした。

ミートアップで社員の雰囲気がとても良いと感じて、率直にここで働いたら楽しそうだと思いました。当時、質問タイムで質問が出なくなったときに、人事の宍道さんが自分で質問して自分で回答していたのを今でも覚えています笑

そして、楽しそうという理由以外に内定を決めた理由が二つあるので紹介します。

就活の軸に当てはまっていた

私は就活の軸に下記ニ点を設定していました。

  • 人生を豊かにするようなサービスを提供する

  • ビジネスサイドとの距離が近い開発体制がある

vivitのビジョンである「心動かす体験を通じて、世界をより豊かに」は、人生を豊かにしたいという私の軸と同じ方向を向いていると感じました。

また、ビジネスサイドとの距離感は個人の測り方でも変わってくるので一概には言えないのですが、同じフロアで働いていることや、仕様についてエンジニアが意見を言えることは、私にとって良い環境だと思いました。

hinataのプラットフォーム化

もう一つは、hinataでプラットフォームを作るという考えに自分も携わりたいと思ったからです。vivitではメディア以外にストアレンタルspotリユースと他のサービスも運用しています。

「メディアでキャンプに興味を持って、キャンプに行きたいと思ったらspotでキャンプ場を予約して、ギアはレンタルで借りて、手ぶらでもキャンプを楽しめる。キャンプギアが欲しくなったらストアで購入もできる。今は個々のサービスとして独立しているけど、プラットフォーム化ができればこれらをシームレスに行えるようになる。」

私はこのプラットフォーム化にとてもワクワクして、エンジニアとして携わりたい!と強く思いました。

インターンの内容

vivitの開発体制に慣れるということでRuby on Railsを使用しているWebメディアのhinataに配属されました。私はもともとRubyを学んでおり、Rubyの技術力を上げたいと希望をだしていたので、そちらも反映していただきました。

インターンに参加して驚いたのが、初日から環境構築を始め、二日目にはタスクを任されたことです。教材を使った基礎学習や先輩エンジニアからの指導などは行わず、hinataの開発業務で慣れるという方針なので、「なんて良い環境なんだ…」と思いました。

氏家さんも入社当時に同様のことを仰っています。

4月1日の入社式で自分がインターンのときと同じmediaチーム(主にhinataの業務)への配属が決まった次の日にタスクが振られ、コードを書くことになりました。

ああ、ベンチャーって本当に良いなって思いました。

vivit.hatenablog.com

タスクについてですが、簡単なものから着手していき、Railsの技術やチームでのGitHubの使い方、リリースの仕方を学んでいきました。常に先輩エンジニアが横についてくれているので、調べてもわからない点は気軽に質問することができました。先輩が会議などで忙しいときはSlackに投稿することでチームのエンジニアに回答してもらっています。

最初は毎日出社していましたが、一ヶ月経った今では開発体制にも慣れてきて、リモートで業務を行っています。インターン生でありながら7回リリースを経験し、ユーザーが実際に触る部分も担当しました。タスクの内容もビジネスサイドから要望が入り、仕様が決まった時点で着手するといったスピード感のあるものも担当しています。

vivitの雰囲気

実際にインターンに参加することで感じたvivitの雰囲気についてご紹介します。

デスクはプロダクトごとで分かれていて、エンジニアは2,3人ずつくらいで固まって作業をしています。ビジネスサイドも開発サイドも混ざったワンフロアなので、静かすぎずうるさすぎず、少しガヤガヤしています。

エンジニアは業務委託の方を含めリモートでの作業が多いので、メッセージで伝わりづらいことはSlackのハドル機能(音声ミーティング)を使用しています。使用頻度があまりにも高いので「ハドれますか?」と「ハドル入ります」のスタンプが最近作成されました笑

終わりに

開発を進める中で、独学では意識していなかった点が顕著に現れていると感じました。命名の仕方やプルリクの出し方、未来の変更を想定した実装など、独学では得られなかった気づきと学びがたくさんあります。まだまだ至らぬ点が多いですが、来年の入社に向けて良いスタートダッシュが切られるよう頑張りたいと思います!

gRPC Server Streamingでメタ情報も一緒にレスポンスする

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

hinata-spotではbackendにGoを、backend間の通信にgRPCを採用しています。

今回はgRPCのServer Streamingでメタデータも一緒にレスポンスする方法をブログにします。

どんな時に使うのか

あまり多くはないかもしれないですが、Server Streamingで通信したいユースケースで、かつそのユースケースで使うメタデータなどもレスポンスしたい場合です。

例えば、1つ1つが容量の大きなデータを複数レスポンスする必要があり、そのデータ総数などのメタデータも送信したい場合などになります。

より具体的な例で以下のようなユースケースで考えてみます。

  • 予約できるプラン(1プランで多くの項目の情報が設定されていると仮定)のリストを返す。

  • またそのプランの総数も必要になる。

上記のユースケースだと、proto定義は以下のようになるかも知れません。

rpc GetPlans(GetPlansRequest) returns (stream GetPlansResponse);

message GetPlansRequest {
  string check_in_date   = 1;
  string check_out_date  = 2;
}

message GetPlansResponse {
  Plan  plan       = 1;
  int64 total_size = 2;
}

message Plan {
  string id          = 1;  // ID
  string name        = 2;  // プラン名
  string description = 3;  // プラン詳細
  // そのほか多くのフィールドがあるとします
}

rpc GetPlansはServer Steamingで定義しています。レスポンスのGetPlansResponseには、Plan型とtotal_sizeが定義されています。 Plan型はstreamingで通信されるので複数回レスポンスされますが、その都度プラン総数もレスポンスすることになってしまいます。

また他のパターンだとプラン総数をレスポンスするrpcを定義するという方法も考えられます。 この場合クライアント側は2つのrpcをcallする必要があります。

rpc GetPlans(GetPlansRequest) returns (stream GetPlansResponse);

// プラン総数をレスポンスするrpc
rpc GetPlansCount(GetPlansRequest) returns (GetPlansCountResponse);

上記のような状況の場合oneofを使うと以下のように定義することがができます。

oneofには以下のように記載されています。

Oneofフィールドは、oneof内のすべてのフィールドがメモリを共有しており、同時に設定できるフィールドが1つだけであることを除けば、通常のフィールドと同じです。

つまりoneofを使って宣言したmessage型はその内部のうちどれか1つのフィールドを返すということを定義できます。 今回のユースケースでoneofを使ってみた場合が以下です。 oneof PlanInfoは、planかtotal_sizeのどちらかをレスポンスします。

rpc GetPlans(GetPlansRequest) returns (stream GetPlansResponse);

message GetPlansRequest {
  string check_in_date   = 1;
  string check_out_date  = 2;
}

message GetPlansResponse {
  oneof PlanInfo {
    Plan  plan       = 1;
    int64 total_size = 2;
  }
}

message Plan {
  string id          = 1;  // ID
  string name        = 2;  // プラン名
  string description = 3;  // プラン詳細
  // そのほか多くのフィールドがあるとします
}

gRPC Server側の実装は以下のようになります。 DBなどからプランを取得してきたあと、まずプラン総数をSendし、その後forでループしながらプランをSendします。

func (s *backendService) GetPlans(req *spot.GetPlansRequest, stream spot.BackendService_GetPlansServer) error {
    params := &usecase.ListSpotPlanParams{
        SpotID: req.GetSpotId(),
    }

  // DBなどからプランを取得してくると仮定
    plans, err := s.usecase.ListPlanUseCase.ListSpotPlan(stream.Context(), params)
    if err != nil {
        return handleError(err)
    }

  // プランの総数を先にSendする
    err = stream.Send(&spot.GetPlansResponse{
        PlanInfo: &spot.GetPlansResponse_TotalSize{
            TotalSize: int64(len(plans)),
        },
    })
    if err != nil {
        return handleError(err)
    }

    for _, plan := range plans {
        err = stream.Send(&spot.GetPlansResponse{
            PlanInfo: &spot.GetPlansResponse_Plan{
                Plan: s.convertPlan(plan), // protoで定義したPlan messageに変換してレスポンスに設定
            },
        })
        if err != nil {
            return handleError(err)
        }
    }

    return nil
}

実際のクライアント側でレスポンスを受け取るコードが以下のようになります。 まずプラン総数を取得します。

その後forでプランを取得していきます。

stream, err := backendService.GetPlans(ctx, &spot.GetPlansRequest{
  // 省略
})
if err != nil {
    return nil, err
}

resp, err := stream.Recv()
if err != nil {
    return nil, err
}

// 取得したプラン総数でスライスのcapを指定するにしている
res := make([]*model.Plan, 0, resp.GetTotalSize())

for {
    // 受信データ量が多いとgRPC転送量エラーになるので、streamで一件ずつ受け取る
    p, err := stream.Recv()
    if err == io.EOF {
        break
    }
    if err != nil {
        return nil, err
    }

    res = append(res, &model.Plan{
        Plan: p.GetPlan(),
    })
}

return res, nil

oneofを使うことで1回のrpc callで異なる種類のデータをそれぞれ取得することができます。 少しスマートな感じがしますね。 また、今後レスポンスにフィールド追加を行いたい場合でもoneof型で宣言したフィールドに追加することで解決できます。

最後に

gRPC Server Streamingでメタデータも一緒にレスポンスする方法について記載しました。誰かの役に立てば幸いです。

vivit では一緒に働くエンジニアを大募集しています 🎉

www.wantedly.com

少しでも興味を持って頂いた方は、是非カジュアル面談の応募をお待ちしております!

新卒がベンチャーでの1年間を振り返って

フロントエンドエンジニアの氏家です。私が新卒としてvivitに入社してから早くも1年が経過しました。

入社してすぐのとき、アウトプットとしてこのエンジニアブログを頻繁に更新していたのが懐かしいですが、今回はそんな私がvivitで1年間働いて、具体的にどんなことをやってきたか四半期ごとに振り返りたいと思います。

4~6月 (チームローテーション期間)

新卒エンジニアとして入社してからまず最初にやったことは、MAU350万を超えるアウトドアメディア「hinata」の開発です。入社前のインターンでもお世話になったmediaチームで、フロントエンド開発に携わりました。

チーム開発による業務に慣れておらず、初歩的なミスや拙いコミュニケーションで迷惑をかけていたと思いますが、いつも優しく対応してくださいました。先輩エンジニアから知識や技術力だけでなく、こういった業務態度も見習っていきたいと思いました。

5月からはキャンプ用品のレンタルサービスを提供している「hinataレンタル」のチームにお邪魔し、念願の React + TypeScript によるフロントエンドの開発に携わりました。

独学での個人開発経験があったとはいえ、業務では分からないことだらけでしたが、都度教えていただきながら業務をこなすことができました。

6月からは主にインフラやプロダクトをまたいだ業務を行う「事業横断チーム」にお邪魔し、各チームの開発環境の整備やサーバ監視ツールの設定などを行っていました。 本格的にフロントエンドエンジニアとして開発業務を行う前に知っておくべきことをたくさん知れて、非常に良い経験ができました。

7~9月 (hinataリユース システム内製化 発足)

アウトドア用品の中古買取・販売を行うサービスを提供している「hinataリユース」の管理システムを内製化することが決まり、7月からリユースチームとして管理画面の作成を行うことが決まりました。

入社して4か月目で新規開発に携わることができ、要件定義から技術選定、開発環境構築を経験させていただきました。

管理画面の第一フェーズ(買い取られた商品を画面上で管理し、外部の中古品販売サイトに出品するところまで)を無事リリースすることができたのですが、現状の開発環境を見直した結果、このプロダクトにはBFFが不要であるという結論になり削除したという経緯があります(詳細は「新規開発においてBFF(Backend for Frontend) を採用すべきか」をご覧ください)。

要件定義の段階でどれだけ考えられていても、実際に走ってみないと分からないということを自ら体感し、完璧な設計は無いということを改めて認識することができました。

10~12月 (リユース開発架橋&ストア兼任)

管理画面の第二フェーズ(中古品の査定からシステムへの登録までを中古品買取の会場で行えるようにするための画面作成)のリリースに向けて、引き続きフロントエンドの開発を行っていました。

徐々に React + TypeScript の開発にも慣れ、コードの可読性やテストコードのカバレッジなどを意識しながら業務をすすめることができました。

また、アウトドア用品の販売を行うオンラインセレクトショップ「hinataストア」の開発業務も担うようになり、リユースチームとストアチームを兼任して開発をしていました。

hinataストアでは Shopify というサブスク制のECプラットフォームを使用しており、ブラウザから商品登録や注文管理、ECサイトのデザイン変更などを行うことができます。

フロントエンドは Liquid というShopify独自のテンプレート言語( eRuby に近い)で記述されており、ノーコードだけでは実現できない変更はエンジニアが対応していました。

社内に Liquid の開発経験があるエンジニアがいなかったため、時間をかけて探り探りで開発を進めていましたが、徐々にできることも増え、ビジネス側の難しい要求にも応えられるようになりました。

ただ、 Liquid での開発体験はあまり良いものではないため、まもなく正式にリリースされる React のフレームワーク Hydrogen でストアのページをイチから作り直したいという願望があります笑

1~3月 (リユース&ストア開発 + 新卒採用お手伝い)

12月末にリユース管理画面の第二フェーズの実装はほぼ完了していたのですが、ビジネス都合によるスケジュールの変更や開発メンバーの入れ替わりなどがあり、リリース前に細かい修正やバックエンドのリファクタリングなどを行っていました。

第二フェーズのリリース完了後は徐々にリユースチームでの開発も減り、hinataストアの事業に注力していきたいという会社の方針もありストアチームでの開発がメインになっていきました。

また、裏では23卒の新卒エンジニア採用を行っており、vivitでは唯一の新卒エンジニアである私が会社説明会や面談などに顔を出し、vivitでの新卒としての働き方について話させていただきました。 自分が1年前と比べてどのように成長したか見つめ直す良い機会でしたし、自分がまだエンジニアとして未熟であることを再認識することができたので、エンジニアとして成長していくためのモチベーション向上にも繋がりました。

終わりに

これまで書いた開発業務以外にも、アウトドア用品の中古品買取イベントに参加したり、hinataレンタルの撮影キャンプにお邪魔させていただいたり、全社的に催されたお花見デイキャンプの実行委員をしたりなど様々な経験をしてきました。

エンジニアリングのスキルを磨きたいということはもちろん、vivitのメンバーとしてアウトドア業界を盛り上げていきたいという思いも強くなったので、積極的にキャンプに行ったりアウトドア関連の情報をキャッチアップしていきたいです。

また、今年の3月に23卒のエンジニア新卒採用を終え、すでに内定者の北條くんがインターンとしてジョインしてくれています。

僕がインターン生だったときと比べてとても優秀なので、追い抜かれないように頑張ろうと思うのと同時に、初心を思い出し、改めてエンジニアリングについて向き合おうと思いました。

1年たってもまだまだ未熟な私ですが、vivitで更なる挑戦をしていきたいです。


vivitでは一緒に働くエンジニアを募集しています。

アウトドア事業に興味がある方、モダンな技術でエンジニアとして成長していきたい方のご応募をお待ちしております!

www.wantedly.com

mediaチームでのasanaの使い方について

こんにちは!技術開発部 media開発チームリーダーの河村です。

今回はvivitで使用しているタスク管理ツールの「asana」について、こんな使い方をしているという事例を交えつつ紹介します。

asanaについて

asanaとはvivitで使っているタスク管理ツールです。

vivitでは入社当時の2020年初頭から無料プランで使っていましたが、当時は技術開発部だけが使用しており、主なプロジェクト管理はスプレッドシートで行われていました。

その後、スプレッドシートより圧倒的にタスク管理がしやすいということで全社的に使うようになりました。

私はいろんな現場でRedmine, Wrike, backlogなど他のツールも使ってきましたが、asanaはUIも直感的だったりタスク間の関係性(親子関係、先行/後続など)がわかりやすい部分がとても気に入っています。

asana.com

asanaのmediaチームでの使い方

mediaチームでasanaを使い始めた時、同じプロジェクトのタスクでも各チーム毎にタスクを持っていてそれぞれのタスクの関係性が見えなかったり、リリース前後でタスクの抜け漏れが発覚するということがありました。

そこでプロジェクトを進める時に以下のようなテンプレート機能を使っています。

このテンプレートでタスクを作ることによって、1つのプロジェクトで必要なタスクを1度に全て揃えることができます。

f:id:Kawam:20220411090152p:plain こちらが親タスクになります。機能追加用テンプレートと書かれた部分にプロジェクト名や実現したい内容が入ります。 説明欄にはこのタスクをなぜやるのか・何がゴールなのかなどの目的を記載し、デザインの関連URLがあれば記入できるようにしています。 vivitではデザインツールはfigmaを使っているので、ワイヤーやfixしたデザインの共有用URLを記入しています。

f:id:Kawam:20220411090218p:plain こちらは親タスクに紐づくサブタスクです。 プロジェクト内で主に対応する必要があるタスクが並べられています。 今回はデザインがいらないなどのケースもあるので、このテンプレートでタスクを作った後に不要なタスクは削除するようにしています。 そうすることでタスクの抜け漏れを防止しています。

f:id:Kawam:20220411090401p:plain こちらは親タスクの実装に紐づくサブタスクです。 実装でもある程度やることが決まっているので必要タスクを並べています。 こちらも不要なタスクがある場合は削除して使っています。

以上のようなテンプレートの使い方をすることでタスクの抜け漏れが防止できたり、タスクの前後関係もわかりやすくなってスケジュール管理がとてもしやすくなりました。

最後に

asanaの機能についてはまだまだ使い切れていない部分も多々あるので、今後も他の機能も見ながらより円滑にプロジェクトが進むような使い方を考えていこうと考えています。

vivit では中途エンジニアも積極募集中です。 少しでも興味を持って頂いた方は、是非カジュアル面談の応募をお待ちしております! www.wantedly.com

GKE に Nginx でレート制限を入れる時の注意点

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

今回はサービスを Dos 攻撃から守るためにレート制限を導入した際に得られた知見をまとめてみたいと思います。

vivit の各サービスは GKE 上で動いており、 GKE Ingress から最初に転送する Pod は基本的に Nginx のリバースプロキシです。 Nginx からユーザーが利用するフロントや API などにアクセスします。

図にすると、以下のようになります。

f:id:tatsurom:20220310155842p:plain

どうやったのか

やり方自体はシンプルで、 Nginx が提供しているレート制限の仕組みをそのまま利用しただけです。 こちらの Nginx 公式ブログ が分かりやすいです。

一部省略や加工を行っていますが、以下のように設定を入れます。

http {
  limit_req_zone $http_x_forwarded_for zone=limit_x_forwarded:50m rate=3r/s;
server {
  listen 80;
  server_name example.com;
  location / {
    limit_req zone=limit_x_forwarded burst=1 nodelay;
    limit_req_status 429;

これだけです。

レート制限の動作確認は必須です。 検証環境への意図的な攻撃には ab を利用しました。

結果の確認は全て Datadog で行っているので簡単でした。 ホストや UA、IP アドレスなどで絞ることで簡単に、どの程度ステータスコード429 で返せたかが分かります。

f:id:tatsurom:20220310162941p:plain

Datadog は決して安くないツールですが、 vivit の開発部では必要で価値あるものにはしっかり投資する文化なのでケチらず使っています。
SRE だけでなく各チームの開発者も日常的に使っており、トラブルシューティングや開発に大いに役立てています!!

この構成の何が問題か

Nginx は Deployment で管理しており Pod のレプリカ数は2以上ですが、レート制限に引っかかるかの閾値はそれぞれの Pod の中だけで判断しています 😇

つまり L4 Service から Pod へのトラフィックは、Pod 間でほぼ等しく分散されるので、意図した数以上のアクセスを許可してしまいかねません。

当然ではありますが、最初は盲点でした。。。

だからといって、例えばレプリカ数が 4 であることを前提として閾値をチューニングすると、それはレプリカ数に依存した設定になってしまい、Pod を気軽に増減できなくなってしまい Kubernetes の良さを一部捨てることになります 🥲

レート制限においては、Nginx は Kubernetes の考え方と相性が悪いとも言えそうです。

結局どうしたのか

かかる工数や、現状受けている攻撃の規模や傾向、Nginx Pod の負荷などを総合的に判断して、今回はこのまま Nginx を使ってレート制限をすることにしました。

適切にチューニングを行った結果、完璧ではないものの、ある程度攻撃に強い構成にできたので目的は達成です!!

改善するなら

Kubernetes 環境でレート制限を入れたいケースは珍しくなく、そのような場合の最も多い構成はサービスメッシュで入れることかなと思います。

Istio を使っている企業さんは多く見受けられます。 公式チュートリアル もありました。

Istio ベースのマネージドな Anthos Service Mesh を検討する方も多そうです。

入れるとなると Kubernetes クラスター全体に影響があることなので今回は見送りましたが、もっと大規模になってきたら検討しても良いのかもしれません。

最後に

vivit では一緒に働くエンジニアを大募集しています 🎉

www.wantedly.com

少しでも興味を持って頂いた方は、是非カジュアル面談の応募をお待ちしております!