Qiita Hackathon 2024 予選通過しました

https://www.ren510.dev/blog/qiita-hackathon-2024

# はじめに

久しぶりに Qiita を更新します。

改めて、2024 年 4 月 [株式会社サイバーエージェント](https://www.cyberagent.co.jp/) に [SRE](https://sre.google/) として新卒入社しました。
現在は、子会社の [株式会社 AbemaTV](https://abematv.co.jp/) にて、Cloud Platform Engineer として [ABEMA](https://abema.tv/) インフラ基盤の運用業務に従事しています。

https://abematv.co.jp/

さて、今回の記事では、先日開催された Qiita Hackathon 2024 に参加をし、予選を通過してきたので、振り返りも兼ねてブログを書こうと思います。

ちなみにハッカソンへの参加は 2 年ぶり？ぐらいになります。
本ハッカソンには、同じく 24 新卒としてサイバーエージェントに入社した他 4 名のメンバーと一緒に参戦しました。

## メンバー紹介

**Team：GAUDI**

<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/276735ce-fed1-0300-8413-cd60b18f9f65.jpeg">

- Flutter フロントエンド：[@nihon_kaizou](https://x.com/nihon_kaizou)
- Flutter フロントエンド：[@icoriha](https://x.com/icoriha)
- バックエンド：[@kei01234kei](https://x.com/kei01234kei)
- バックエンド：[@kiton_1938](https://x.com/kiton_1938)
- インフラ / Cloud Platform（👈 担当）：[@ren510dev](https://x.com/ren510dev)

# Qiita Hackathon とは

https://qiita.com/official-campaigns/hackathon/2024

## 開催概要

Qiita Hackathon とは、日本最大級のエンジニアコミュニティ Qiita（Qiita 株式会社）が開催しているハッカソンです。
本ハッカソンはオンラインでの予選と、予選を通過した計 10 チームによるオフライン会場での本選の計 2 回に渡り開催されます。

**最優秀賞の賞金は 50 万 だそうです！**

【予選】

- 2024 年 9 月 21 日(土)・2024 年 9 月 22 日(日)
- オンライン開催

【本選】

- 2024 年 10 月 19 日(土)・2024 年 10 月 20 日(日)
- オフライン開催（東京都渋谷区 GMO インターネットグループ グループ第 2 本社）

## 審査基準

審査基準は以下の 4 つとなっています。

- **課題設定**
  テーマに沿った課題を設定している作品であるか

- **課題解決**

  課題解決や価値提供をしている作品であるか

- **技術力**

  高度な技術が使われ、優れた UX を提供している作品であるか

- **プレゼンテーション**

  説得力のある内容を時間内に伝えられているか

## テーマ

予選・本選でテーマが分かれており、ともに当日発表されます。
また、成果物は、Web サービス、ネイティブアプリケーション、IoT 等、プログラミングを用いたものならば形態不問でした。

今回の予選のテーマは、ずばり

![theme.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/4c31a3f5-e8f6-2d2c-9673-c8d8512b995a.png)

でした。

ん、...どういうこと？

テーマに対する詳細な説明はありませんでした 😇
どう解釈するかも含めてハッカソンしてね〜ということらしいです！

## 審査スキーム

出場 81 チーム が A / B 2 つのブロックに分かれて予選を競い、上位 5 チームずつ選出された 計 10 チーム が本選進出となります。

# 制作物

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/29a34501-fab3-4cb6-f9ef-cd097c0f252f.png)

さて、まずテーマですが、『オープンって何...？？』これが正直な感想でした。

時間も限られているので、スケジュールも意識しながらチームでブレストしました。

最終的なアイデアとして、我々のチームは **"ヘルプマーク"** に着目することにしました。

例えば、公共交通機関を利用すると、赤十字マークのラベルをカバンやリュックに提げている人を見かけることがあるかと思います。

![bannar.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/8bfe0502-9623-9b82-babb-38a7b5d3c1f1.jpeg)

このラベルはヘルプマークといって、援助や特別な配慮を必要としている人が、そのことを周囲の人に知らせるためのものです。見かけた際には、席を譲ったり、声を掛けたり、**周りが配慮してあげる** ことが要求されます。[^1]

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/7fab5a6a-0b64-71d5-53c9-265d2cec77b3.png)

しかし、昨今ではあらゆる場面で多くの人がスマートフォンに注視しており、近くにヘルプマークを付けた人がいても気づかないことが多いのではないでしょうか。また、ヘルプマークの存在自体を知らない人もいるかもしれません。

ヘルプマークの課題は、いかにしてヘルプマークに気づいてもらえるか（その存在をオープンにしていくか）という根本から解決する必要があるということです。

東京都福祉局では、近年、民間企業への働きかけを活発化させる等、ヘルプマーク普及のための啓発活動が進められているようです。

このような背景を受け、デジタル化したヘルプマークが普及すれば、公共の福祉や医療の面での貢献が期待できるのではないかと考えました。

そこで、ヘルプマークにビーコンを仕込んでおき、Bluetooth（BLE）による近距離無線通信を用いて、近くのスマートフォンに検出させる仕組みとサービスを作ることにしました。

難しく説明してもあれですが、似たような仕組みに、[AirTag](https://www.apple.com/airtag/) によるトラッキングがあります。AirTag は BLE や UWB を使用して、近くの Apple デバイス（他人の所有物を使って）と通信し、これによって AirTag の現在の位置を推定します。

この仕組みを利用することで、ビーコンを搭載したヘルプマークを持った人が近くに来ると、近隣のスマートフォンが検出可能なため、要介助者の存在に気づくことができます。

つまり、**『ヘルプをオープン』** できるようになります。

![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/2b409c91-bb91-26f9-d04d-0b7aebf637c2.png)

## へるぷくん+

話題や機能の展開は色々できそうですが、所詮 2 日間しかないのでミニマムのアイデアで開発を進めることにします。

<font color="Red">サービス名：『へるぷくん+』</font>

**サービスモデル（コア機能を意識）**

- 介助者を要求するケアラー（Carer）と、ケアラーを助けるヘルパー（Helper）をペルソナ定義
- ヘルプマーク所持者であるケアラーは、介助内容をアプリケーションに登録
- ビーコン検出したヘルパーは、内容に応じた介助動作に移る（誰でも正義のヒーローになることができる！）

**実装機能（シンプルに止める）**

- BLE ビーコン検出機能
- ユーザサイド情報の管理機能

![demonstration-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/af0feb00-4d7b-2587-654c-bd221563f532.png)
![demonstration-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/c50cfc87-70e0-1a18-3fa5-004a5345f470.png)

**プレビュー**

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/3586081d-2605-0f99-b48e-325c0dbe78d1.gif">

# 選定技術

- ネイティブクライアント

<img width="80" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/52e237c9-cc3d-e414-b038-7f3766b5eedf.png">
<img width="80" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/fdeecb72-5f0e-a51d-efb9-eb410ae5302f.png">

- バックエンド

<img width="80" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/1ea2e273-34af-84e7-f160-0ae71d71c8f1.png">
<img width="80" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/542b5dad-fa6b-7b60-8a67-6b5bfa30a2a7.png">

- インフラ / Cloud Platform

<img width="80" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/cfce9f6c-bc1f-66d5-9832-7209731bbca5.png">
<img width="80" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/260848c5-8a3b-45ee-3dc4-88efb261b071.png">

モバイルアプリケーションのネイティブクライアントは、[Flutter](https://flutter.dev/) を使用し、バックエンドは [Go](https://go.dev/) で実装しました。クライアント・バックエンド間の通信には [gRPC](https://grpc.io/) を使用しています。

通常であれば、[RPC](https://datatracker.ietf.org/doc/html/rfc5531) は異なるコンポーネント（マイクロサービス）間でメソッドをコールするために用いることが一般的だと思いますが、今回は、[Protocol Buffer](https://protobuf.dev/)（IDL）を用いたコード生成自動化の恩恵を受けるために採用しました。これにより、ものの短時間で数十個程度の API を一気に生やすことができました。

また、実際のサービス運用も視野に入れ、拡張性を意識したインフラを構築するべく、GKE をベースとしたエコシステムを Cloud Platform として整備しました。興味本位で、データベースには [Cloud Spanner](https://cloud.google.com/blog/topics/developers-practitioners/what-cloud-spanner?hl=en) を使用しています。

担当したインフラ部分について少し深ぼって紹介してみたいと思います。

# アーキテクチャ

ざっくりとしたインフラ構成を紹介します。

![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/758ec525-355a-4e8b-cf6d-a6fe297549d5.png)

GKE をベースに CI/CD はじめ、軽く Observability 基盤まで構築してみました。
その他、ユーザ認証には Firebase Authentication や Cloud Run Functions を使用しています。

- Kubernetes Engine：[GKE Standard editions](https://cloud.google.com/kubernetes-engine/docs/concepts/kubernetes-engine-overview)
- データベース：[Cloud Spanner](https://cloud.google.com/blog/topics/developers-practitioners/what-cloud-spanner?hl=en)
- キャッシュ・通知：[Memorystore for Redis](https://cloud.google.com/memorystore/docs/redis/memorystore-for-redis-overview) / [Redis PubSub](https://redis.io/docs/latest/develop/interact/pubsub/)
- CI：[GitHub Actions](https://github.co.jp/features/actions)
- CD：[ArgoCD](https://argo-cd.readthedocs.io/en/stable/)
- モニタリング：Addon metrics-server + [Kube-Prometheus-Stack](https://github.com/prometheus-operator/kube-prometheus)
- ロギング：Addon Fluentbit + Cloud Logging（[EFK-Stack](https://www.elastic.co/blog/monitoring-kubernetes-with-the-elastic-stack-using-prometheus-and-fluentd/)）
- マニフェスト管理：[Kustomize](https://kustomize.io/) / [Helm](https://helm.sh/)
- ネットワークプロキシ：[Envoy](https://www.envoyproxy.io/docs/envoy/latest/intro/what_is_envoy)（gRPC 負荷分散用途）

## Envoy で gRPC 通信をプロキシする

アーキテクチャを構築する上で一点注意しなければならなかったのが、GKE Ingress を使用した際の gRPC 負荷分散に関してです。

GCP において、特に GKE Ingress を使用した L7 の負荷分散を行う際は、GCLB が gRPC のヘルスチェックに対応していないため、トラフィックをバックエンドワークロードに流すことができません。

<img width="2000" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/babb6c8e-241b-73de-f96a-386da082e004.png">

ちなみに、AWS ALB は gRPC のヘルスチェックをサポートしています。おそらく、ALB 自体が Nginx Ingress Controller ベースで、リバースプロキシを内部的に噛ませることで gRPC に対応しているのではないかと思われます。

https://aws.amazon.com/jp/builders-flash/202212/alb-grpc-balancing/

そのため、GCLB で gRPC のヘルスチェックをパスするために、別の方法を考える必要があります。

考えられる対応方法としては以下 3 つがあります。

**① アプリケーションに HTTP/2 で応答するヘルスチェック用のエンドポイントを追加する**

こちらは、[grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) 等を用いて、gRPC リクエストを REST に変換して HTTP/2 のヘルスチェックに対応します。
また、GCLB はデフォルトのヘルスチェックプロトコルとして、HTTP/1.1 を使用するため、このようなケースでは別途、Service リソースにアノテーションを明示して、HTTP/2 によるヘルスチェックを構成する必要があります。

```yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    cloud.google.com/app-protocols: '{"my-port":"HTTP2"}'
```

https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-http2

**② アプリケーション Pod の前段にリバースプロキシ Pod を置きヘルスチェックに応答する**

こちらは、全てのリクエストを一旦、リバースプロキシとして機能する Pod が受け取り、その後、アプリケーション Pod にフォワーディングするというものです。

**③ アプリケーションの Pod にサイドカーとしてリバースプロキシコンテナを配置してヘルスチェックに応答する**

こちらは、サイドカーコンテナとしてデプロイされたプロキシ Pod がヘルスチェックに応答し、それ以外のリクエストをアプリケーション Pod にフォワーディングするというものです。
② との違いは、サイドカーとして機能するため、クラスタに対して単一の Pod としてデプロイできる点です。

ベストプラクティスに沿って考えれば、自ずと選択肢は 1 or 3 のパターンになると思います。
しかし、1 はネットワーク関連の事情をアプリケーション側で面倒見てあげる必要があるため、あまり良い方法とは思えず、結果、ネットワークプロキシの処理はインフラ側で担える 3 の方法を取ることにしました。

3 では、GCLB のヘルスチェックルールをうまく利用します。

GCLB（GKE Ingress）は次の流れでヘルスチェックを作成します。

1. BackendConfig があればそれを使う
2. Pod に readinessProbe が設定されている、かつ条件を満たす場合それを使う
3. 1, 2 を満たさない場合、デフォルト値（`/`）を使う

https://cloud.google.com/kubernetes-engine/docs/concepts/ingress#health_checks

まず、以下のような Envoy Config を用意し、サイドカーコンテナをデプロイします。

```yaml
static_resources:
  listeners:
    - name: listener_https
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 8443
      listener_filters:
        - name: envoy.filters.listener.tls_inspector
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                # （省略）
                http_filters:
                  - name: envoy.filters.http.lua
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
                      inline_code:
                        | ## ヘルスチェックに応答するための Lua スクリプトを埋め込む
                        package.path = "/etc/envoy/lua/?.lua;/usr/share/lua/5.1/nginx/?.lua;/etc/envoy/lua/" .. package.path
                        function envoy_on_request(request_handle)
                          if request_handle:headers():get(":path") == "/healthz" then
                            local headers, body = request_handle:httpCall(
                              "local_admin",
                              {
                                [":method"] = "GET",
                                [":path"] = "/clusters",
                                [":authority"] = "local_admin"
                              },
                              "",
                              5000
                            )

                            if body == nil then
                              request_handle:logInfo("Health check failed: no body returned")
                              request_handle:respond({[":status"] = "503"}, "unavailable")
                              return
                            end

                            request_handle:logInfo("Health check body: " .. body)

                            local function is_cluster_healthy(body)
                              local pattern = "grpc_backend::%d+%.%d+%.%d+%.%d+:%d+::health_flags::healthy"
                              return string.match(body, pattern) ~= nil
                            end

                            if is_cluster_healthy(body) then
                              request_handle:respond({[":status"] = "200"}, "ok")
                            else
                              request_handle:logInfo("Health check failed: grpc_backend not healthy")
                              request_handle:respond({[":status"] = "503"}, "unavailable")
                            end
                          end
                        end
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
          transport_socket:
            name: envoy.transport_sockets.tls
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
              common_tls_context:
                alpn_protocols:
                  - h2 ## HTTP/2 を受け入れる
                  - http/1.1
                tls_certificates:
                  - certificate_chain:
                      filename: "/etc/envoy/certs/tls.crt"
                    private_key:
                      filename: "/etc/envoy/certs/tls.key"

  clusters:
    - name: grpc_backend
      connect_timeout: 0.25s
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      typed_extension_protocol_options:
        envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
          "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
          explicit_http_config:
            http2_protocol_options:
              max_concurrent_streams: 100
      load_assignment:
        cluster_name: grpc_backend
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: localhost ## localhost:8080 にトラフィックを転送する
                      port_value: 8080
      health_checks:
        - timeout: 1s
          interval: 1s
          no_traffic_interval: 1s
          unhealthy_threshold: 2
          healthy_threshold: 2
          tcp_health_check: {}
```

次に BackendConfig を定義して、ヘルスチェック先を `/healthz`（z-pages）に向けます。

```yaml
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  name: envoy-proxy-backend-config
spec:
  healthCheck:
    checkIntervalSec: 30
    timeoutSec: 10
    healthyThreshold: 1
    unhealthyThreshold: 3
    type: HTTP2
    requestPath: /healthz
    port: 8443
```

これで、ヘルスチェック（downstream）に対しては Envoy が 200 応答を返し、それ以外の gRPC 通信は upstream（アプリケーション Pod）に流してくれます。
また、Envoy は常に upstream が healthy かを確認してからヘルスチェックに応答するため、アプリケーション Pod が何らかの理由により停止した場合はトラフィックを、他の Pod へ転送するように GCLB に促すことができます。

以上より、1（BackendConfig）を利用してヘルスチェックをパスさせることができました。

実際に Ingress を describe すると、バックエンドステータスが `HEALTHY` になっていることが確認できます。

<img width="2000" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/6501e6a7-2b25-7fdc-52fb-3d5c1fcf8a16.png">

今回は、突貫工事的に Envoy でプロキシする方法をとっていますが、実際の運用でバックエンドが複雑化してきたら、ちゃんとサービスメッシュを整備した方が良さそうです。

# おわりに

今回は久しぶりにハッカソンに参加をしたので、振り返りも兼ねて Qiita を書いてみました。
アイデア面でも技術面でも久しぶりにサービスについて考える機会になったし、技術についても深い学びがありました。

そして何より予選を通過することができて良かったです！

<img width="2000" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705806/63551ecb-5a68-82ed-dd00-246a7fec81b8.jpeg">

本選でも新たな学びを作りつつ、最優秀賞を狙って頑張りたいと思います！

**この記事が良ければ ❤️ を頂けると励みになります 🙏**

# 他の参加者ブログ

他のグループも非常にレベルの高いプロダクトがいくつもありました！
いくつか他の参加者のブログを紹介したいと思います！

- [過去の経験を活かして効率よく(?) 開発を進めたハッカソンの話](https://qiita.com/satorilab/items/97e047a93eb31b1e7365)
- [学生エンジニアが歯医者とマーケターと一緒に初めてハッカソンに参加した話](https://qiita.com/ryuprogrammer/items/d86bcb2a0c4688d03ce8)
- [ハッカソンのアイデア出しで 5 時間かかって大苦戦した話【QiitaHackathon2024】](https://qiita.com/_sayako21/items/bac588771920ef44e682)
- [【QiitaHackathon2024】『Halico』とその技術構成について](https://qiita.com/tsuchi-noko/items/210d432e3e70600af9b0)
- [AI で気になるあの子の気持ちをオープンにするアプリを作りました！【QiitaHackathon2024 参加体験談】](https://qiita.com/hiroto_en/items/77ea02c5c26b4cc2eeb1)

[^1]: https://www.fukushi.metro.tokyo.lg.jp/shougai/shougai_shisaku/helpmark.html