サイボウズサマーインターン2023 プロダクトセキュリティコース 開催報告

こんにちは!Cy-PSIRTの田口です。
本記事はサマーインターンシップ2023 プロダクトセキュリティコースの開催報告です。 今年も去年に続き、8月と9月に全2ターム、フルリモートでインターンシップを開催しました。

概要

プロダクトセキュリティコースは第1タームを8月28日〜31日、第2タームを9月11日〜14日で開催し、それぞれ4名のインターン生にご参加いただきました。 インターンでは、普段Cy-PSIRTが行っている業務をもとにコンテンツを構成しており、以下のような内容をインターン生の皆さんに体験していただきました。

  • 製品理解・ハンズオン
  • 脆弱性検証
  • 脆弱性評価
  • 外部通報対応

Cy-PSIRTの具体的な業務内容については以下の記事で紹介しています。

blog.cybozu.io

また、コンテンツで触れる業務以外のCy-PSIRTが行なっている取り組みの紹介や、社内環境を一部お見せしながらチームの雰囲気や実際の業務の進め方を知っていただく時間も今年から新たに設けました。

製品理解・ハンズオン

はじめに、サイボウズ製品やWebアプリケーションの脆弱性について学んでいただきました。 Cy-PSIRTの業務ではあらゆる場面で製品理解が重要になります。その第一歩として各製品の機能や使い方について簡単に説明を行いました。 その後、脆弱性の概要や原理について学び、ハンズオンを行なっていただきました。ハンズオンでは、過去サイボウズ製品に存在していた脆弱性(改修済み)を再現させ、脆弱性の挙動を確認していただきました。

脆弱性検証

この時間では、Cy-PSIRT内での脆弱性検証フローである以下の業務を順番に演習し、脆弱性検証の一連の流れを体験していただきました。

  • 要件確認
  • 試験仕様書の作成
  • 脆弱性検証

まずは 要件確認を行なっていただきました。要件確認とは、開発チームが実施した要件を確認し脆弱性検証の要否を判断する業務です。Cy-PSIRTでは、通常、開発によって生じる製品の差分に対して脆弱性検証を実施しています。そのため、要件確認は脆弱性検証フローの中で最初に行う必要がある業務です。 演習では、要件の実例を挙げながら、脆弱性検証が必要となる判断基準や考え方を学んでいただきました。

要件確認の様子(第1ターム)
要件確認(第1ターム)

次に、要件確認によって脆弱性検証が必要であると判断した要件について試験仕様書を作成します。 検証対象のリクエストについて、パラメータ単位での検証項目、権限、その他検証観点を漏れがないよう洗い出し、kintoneアプリに書き出していただきました。

試験仕様書を作成後、いよいよ検証に入ります。 作成した試験仕様書の内容をもとに、Burp Suiteを使いながら一つずつ検証を行います。 インターン生の中には、アジャイル開発における脆弱性検証や自動化に関心を持つ方、QAとPSIRTの試験範囲の違いに疑問を抱く方がいらっしゃいました。また、インターン期間中に脆弱性を見つけることを目標に掲げて検証に取り組む方もいらっしゃいました。インターン生は、検証やツールの使い方など技術的な面以外にも、脆弱性や品質保証に対する考え方など多くの気づきを得ることができたようです。

ランチ

お昼はCy-PSIRTのメンバーとインターン生でランチをしました。 一緒にご飯を食べながら気軽に雑談することで、Cy-PSIRTメンバーもインターン生のことをより知ることができました。 インターン生の皆さんも、チームの雰囲気を知る機会になっていただけたら嬉しいです。

ランチ(第1ターム)
ランチ(第1ターム)
ランチ(第2ターム)
ランチ(第2ターム)

脆弱性評価

続いては、脆弱性評価の時間です。サイボウズでは、脆弱性の深刻度をスコアリングする手法の一つであるCVSSv3を用いて脆弱性評価を行なっています。インターン生の皆さんには、CVSSv3の考え方やスコアの付け方を学んでいただき、過去サイボウズ製品に存在していた脆弱性を評価していただきました。

脆弱性評価(第2ターム)
脆弱性評価(第2ターム)

後半では、インターン生の皆さんが算出したスコアや考え方についてお互いに共有しました。 他の人の意見を聞いて、想定するシナリオや脆弱性の捉え方が人によって異なることを実感していただけたと思います。 Cy-PSIRT内でも、各評価項目に対する考え方やスコアの付け方に関する議論がよくありますが、他の人の意見を聞いたり議論を行うことで、より公正な評価を行うことができます。 この時間を通して、スコアをつけることの難しさやシナリオの考え方など新たな学びを得る機会になっていれば嬉しいです。

外部通報対応

最後のコンテンツは外部通報対応です。この時間では、サイボウズが運営している脆弱性報奨金制度の対応業務を体験していただきました。 外部通報対応は、脆弱性の再現確認や検証、脆弱性評価、また製品に対する理解など、これまでの時間で体験した業務の知識が必要となる集大成です。 はじめに、脆弱性報奨金制度の仕組みや実施することの意義を学んでいただき、その後「トリアージ」「再現確認」「評価」の一連の流れを演習していただきました。 このコンテンツを通して、脆弱性報告の書き方について気づきを得た方やバグハンティングに興味を持っていただいた方もいました。脆弱性報奨金制度の運営側について知る良い機会になったのではないでしょうか。

その他の紹介

最終日には、インターン生にCy-PSIRTの取り組みや普段の業務の雰囲気を知ってもらう時間を設けました。 Cy-PSIRTではコンテンツで紹介した業務の他にも、たくさんの活動や取り組みを行っています。 今回は以下の取り組みについて紹介しました。

  • 脆弱性検証の自動化
  • モバイル検証
  • インシデントハンドリング
  • チーム内での勉強会
  • WG / タスクフォース*1
  • チーム内イベント
  • 社外のセキュリティイベント/カンファレンス

インターン生は自動化やモバイルなど検証に関わる部分に興味を示している方が多かったようです。
それぞれの取り組みについて、目的や背景、現状の運用について説明しました。 また、チーム内のイベントや社外イベントへの参加を通じた情報収集や交流についても簡単にご紹介しました。

さらに、業務環境のスクリーンショットを一部紹介し、普段の業務の様子を知っていただきました。 脆弱性検証、評価などコンテンツで学んだ業務について、実際の運用や議論の様子をご覧いただきました。 また、開発チームとCy-PSIRTとのやりとりも紹介し、ユーザー企業のセキュリティチームとしての役割についても理解を深めていただきました。 こうした時間を通じて、業務のイメージやチームの雰囲気がより鮮明になったり、新たに得られる気づきがあれば嬉しいです。

成果報告会・懇親会

最終日の夕方は、kintoneアプリを使った形式で成果報告会を実施しました。 インターン生の皆さんには、「やったこと」「学び、気づき」「やってみたいこと」「感想」をベースに、各コンテンツについて事前に振り返りを記入していただきました。 成果報告会では、その振り返りの内容をもとに各コンテンツについての学びや感想を共有していただきました。

成果報告会(第1ターム)
成果報告会(第1ターム)
成果報告会(第2ターム)
成果報告会(第2ターム)

夜には懇親会を行いました。去年に続き、Cy-PSIRTメンバーに聞いてみたいことを事前に登録していただき、その内容をもとに雑談しながら夕食を食べました。 業務のことから業務以外の話題までとても盛り上がり、非常に楽しい時間となりました。

いただいた感想

今回のインターンでは、第1タームと第2タームそれぞれ4名の方にご参加いただきました。 参加者からいただいた感想を一部ご紹介いたします。

メンターさん以外の方も含めた社員の皆さんや他のインターン生、青野さんともお話させていただく機会があり、チームワークを大切にするサイボウズの風土を感じられました。また、座学と演習のプログラム全体が繋がっていて、手を動かしながらCy-PSIRTや事業会社のセキュリティ業務について理解していけるインターンでした。参加して本当に良かったです。


実際の業務の流れを体験することで、プロダクトセキュリティエンジニアという仕事への解像度が高くなりました。脆弱性評価や外部通報対応を通して、セキュリティの楽しさと難しさを再実感しましたし、メンターさんからのフィードバックをいただくことでより理解が深まりました。また、4日間でサイボウズの文化やチームワークの強さを知ることができたという意味でも、非常に参加して良かったと思いました。


今回のインターンでは、サイボウズの「チームワーク」について特に注目して参加していました。参加する前は、チーム内での助け合いが活発なのかなと考えていたが、実際には、チームを超えて助け合いが行われていました。チーム内だけでなく、サイボウズ社内でチームワークを発揮し助け合ってチームワークを高められる最高のグループウェアを作る、という社風がとても素敵だと感じました。大変有意義な4日間でした。非常に楽しかったです。

おわりに

今年も去年に続き、オンラインでの開催となりましたが2タームとも無事に実施することができました。 4日間と短い間でしたが、会社やチームの雰囲気、プロダクトセキュリティについて知る良い機会となったと思います。 また、インターン生同士が積極的に交流を深める様子も見られ、メンターとして大変嬉しく思いました。

インターンで用意されているコンテンツは、実際の業務をもとにした実践的な内容で構成されています。 ユーザー企業のセキュリティチームとしての役割や、Cy-PSIRTならではの業務についても学んでいただけたと思います。 インターン生の皆さんが、この経験や学びを今後の活動に役立ててくださることを願っています。今後の活躍を楽しみにしています!

Cy-PSIRTでは一緒に働く仲間を募集中です。ご興味のある方は以下をご覧ください。

cybozu.co.jp

*1:Cy-PSIRTではチーム内の課題や問題意識をもとに有志で小さなチームを作り様々な活動を行っています。

サイボウズサマーインターン 2023 報告 〜 プラットフォームコース

cybozu.com Cloud Platform 部の新井です。 Cloud Platform 部では現在、旧クラウド基盤上で動作している製品を、Kubernetes ベースの新基盤に移行させるためのコンポーネントを開発・運用しています。 今年 2023 年、Cloud Platform 部ではプラットフォームコースという新しいコースで、製品の新基盤移行に関われるインターンを開催し、平地さんに参加いただきました。 そしてインターンの中では、Envoy を用いた TLS のクライアント認証に関する技術検証を行なっていただきました。 ただし、単純なクライアント認証ではなく、

  • 認証局が複数個あり、さらに動的に増減する
  • コネクションごとに、クライアント証明書の検証に利用する認証局が異なる

という要件を満たすような実装を行っていただきました。 本記事では、インターンの内容とその成果をご紹介します。

インターンの進め方

インターンの開催期間は、8/28(月)から 9/8(金)までの2週間で、オンラインでの開催でした。 1日目から2日目は人事のオリエンテーションや環境構築、インターンで取り組む課題の説明を行いました。 いくつか用意していた新基盤移行のための実タスクのうち、参加者の希望を聞いて、冒頭でも紹介した Envoy を用いたクライアント認証の技術検証タスクをやってもらうことにしました。

ただ、メンターとしては、このタスクはかなり困難な挑戦になると予想していました。 というのも、Envoy は導入するかどうかすら未検討ということもあって、メンター陣が Envoy に詳しくなかったからです。 また、参加者自身も、Linux や Kubernetes に関する知識は持っていたのですが、TLS の仕様についてそれほど詳しいわけではありませんでした。 しかし、参加者自身が積極的に Envoy の調査・コーディングをして、その状況を報告してくれたので、その内容について議論するうちに、ともに Envoy の機能について理解を深められました。 また、TLS の仕様や社内における TLS の利用についてはメンターからレクチャーを行ったり、調査に行き詰まった時は、適宜 Zoom を繋いで一緒に悩んだりしながらタスクを進めました。 結果として、当初メンターが想定していた以上の成果が得られたと思います。

その他、タスクを進める以外の業務としては、メンターが出席する会議に一緒に出てもらい、製品移行時のアーキテクチャを検討する議論や、基盤の運用に関する議論などを見てもらいました。

タスクの内容

まずは、今回取り組んだタスクである「Envoy を用いた、TLS のクライアント認証の際に使用する認証局を動的に選択する方法の検証」が必要になった背景と関連用語について説明します。

TLS におけるクライアント認証

TLS では、X.509 証明書を用いたサーバ認証を行います。 サーバ認証では、クライアントは、サーバから証明書を受け取ると、今通信しているサーバは本当にその証明書の所有者か、その証明書は信頼された認証局(CA:Certificate Authority)から発行されたものかなどを検証します。 クライアント認証はその逆で、サーバがクライアント証明書を受け取り、その検証を行います。

サーバ認証が必須なのに対し、クライアント認証は任意です。 言い換えると、クライアント認証を行わないときは、クライアントがサーバを一方的に認証し、クライアント認証を行うときは、クライアントとサーバが相互に認証します。 そのため、この方式を mutual TLS(mTLS)と呼びます。

mTLS
mTLS

社内での利用

弊社のクラウド基盤である cybozu.com の内部に、クライアント認証を行うコンポーネントがあり、これを Kubernetes 基盤に移行する必要があります。 しかし、このコンポーネントのクライアント認証は少し特殊で、アクセス元の SNI(Server Name Indication)*1 ごとに、クライアント証明書を検証する CA を切り替えます。 さらに、アクセス元の SNI のバリエーションは動的に増減するので、その対応ルールもまた動的に更新しなければなりません。

少しわかりにくいので、具体的に説明します。 例えば、Apple と Banana という 2種類のクライアントがいるとします。 Apple は apple.example.com という SNI でサーバにアクセスし、banana.example.com という SNI でサーバにアクセスします。 これらを認証するために、クライアント証明書を配布するのですが、Apple に配布する証明書は Apple CA から、Banana に配布する証明書は Banana CA からというように、SNI ごとに異なる CA から発行された証明書を配布します。 そのため、Apple からのリクエストが来た時には Apple CA の証明書を使って、提示されたクライアント証明書が Apple CA から発行されたものかどうかを検証し、Banana からのリクエストには、 Banana CA 証明書を使って検証しなければなりません。 さらに、将来 chocolate.example.com という SNI でアクセスしてくる新たなクライアントが現れるかもしれない、という状況です。

社内における mTLS の利用

この仕組みを Kubernetes 基盤上でも提供し続けるためには、Nginx のようなロードバランサを単純には使えない*2ので、他の選択肢を検討する必要がありました。 そこで、柔軟にプロキシの設定を変更できる Envoy について調査することにしました。

Envoy

Envoy は、非常に柔軟な設定を書けるプロキシとして動作します。 それだけでなく、xDS API と呼ばれるインタフェース経由で、動的にその設定を変更できるのが大きな特徴です。 ここで、xDS API 経由で動的に設定を配信するサーバをコントロールプレーンと呼びます。 それに対して、受け取った設定を元に実際にプロキシとして動作するコンポーネントをデータプレーンと呼びます(この文脈では Envoy がデータプレーンに相当します)。

本節では、シンプルかつ静的な Envoy の設定例を紹介した後、クライアント認証の設定例について説明します。

Envoy の設定例

まずは、Envoy の公式チュートリアルに沿って、静的な Envoy の設定方法を紹介します。 また、Installing Envoy の手順で Envoy がインストールされていると仮定します。

Envoy の静的な設定を書くのに必要なことは、listeners と clusters リソースを static_resources 内で指定することです。 設定例は以下の通りです。 また、設定の中でも重要な点、補足が必要と感じた点はコメントで補足を入れています。

static_resources:

  listeners:
  - name: listener_0
    # Envoy がリッスンするアドレスとポートを指定
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 10000
    filter_chains:
    # プロキシのルールを指定
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          access_log:
          - name: envoy.access_loggers.stdout
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
          http_filters:
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              # ワイルドカードが指定されているので、任意のドメインへのアクセスはこのルールに沿って処理される
              domains: ["*"]
              routes:
              # / という prefix が付いたリクエストは、www.envoyproxy.io にホストが書き換えられる
              - match:
                  prefix: "/"
                route:
                  host_rewrite_literal: www.envoyproxy.io
                  # cluster の設定は service_envoyproxy_io を利用
                  cluster: service_envoyproxy_io

  clusters:
  # www.envoyproxy.io との接続に関する設定
  - name: service_envoyproxy_io
    # DNS の設定
    type: LOGICAL_DNS
    # Comment out the following line to test on v6 networks
    dns_lookup_family: V4_ONLY
    load_assignment:
      cluster_name: service_envoyproxy_io
      # 実際に接続するアドレスの指定
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: www.envoyproxy.io
                port_value: 443
    transport_socket:
      # TLS の設定
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
        sni: www.envoyproxy.io

次に、この内容で動作確認を行います。 以下のコマンドにより、この設定ファイルを、Envoy に読み込ませて起動します。

$ wget https://www.envoyproxy.io/docs/envoy/latest/_downloads/92dcb9714fb6bc288d042029b34c0de4/envoy-demo.yaml 2> /dev/null
$ ./envoy -c envoy-demo.yaml
[2023-10-26 02:06:55.603][3223099][info][main] [source/server/server.cc:413] initializing epoch 0 (base id=0, hot restart version=11.104)
...

別の端末で Envoy にアクセスします。

$ curl -i localhost:10000
HTTP/1.1 200 OK
accept-ranges: bytes
age: 345
cache-control: public,max-age=0,must-revalidate
content-length: 15388
content-security-policy: frame-ancestors 'self';
content-type: text/html; charset=UTF-8
date: Thu, 26 Oct 2023 02:07:42 GMT
etag: "9ba8c8215f215781dc8a29c164ee46a3-ssl"
server: envoy
strict-transport-security: max-age=31536000
x-nf-request-id: 01HDMVSJSNGKQXG9ZRCAAJ9J0W
x-envoy-upstream-service-time: 279

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Envoy proxy - home</title>
...

この結果から、localhost:10000 でリッスンしている Envoy にアクセスすると、プロキシ先である Envoy proxy の公式ページからのレスポンスが返っていることがわかります。

タスクに対するアプローチ

本タスクは、あらかじめ方針が決まっていたわけではありませんでした。 そのため、インターン期間中に参加者自身に様々な機能について調査してもらい、実装方針を決めるところから取り組んでもらいました。 具体的には、ext_authz(External authorization filter)、SDS(Secret Discovery Service)などの機能を試してもらい、最終的に、LDS(Listener Discovery Service)を利用した方法がうまくいきそうだということがわかりました。 そして、参加者に要件を満たす LDS のコントロールプレーンを実装してもらいました。

本章では、これらの機能について試したことと、最終的にうまくいった方法を説明します。

ext_authz(External authorization filter)の利用

ext_authz は、認証を外部のサーバに切り出せる機能です。 Envoy には、Authorization service という API 仕様が決められています。 Envoy は、クライアントからのリクエストを受けると、この仕様に沿ってリクエストのメタデータを認証サーバに送信します。 よって、そのデータを元に認証を行い、結果を Authorization service API で送信するロジックを実装することにより、下図のように外部サーバによる認証を実現できます。

ext_authz

この機能を使って、クライアントから送られてきた証明書とアクセス先の SNI を認証サーバに転送し、SNI に応じて検証に利用する CA を切り替えるロジックを実装するというアプローチを試しました。 しかし、この試みは失敗に終わりました。 元々、ext_authz は主に HTTP のレイヤにおける認証のための機能で、TLS のレイヤのものではありませんでした。 そのため、クライアント証明書を認証サーバに送れることは確認したのですが、TLS 自体は前段の Envoy で終端されてしまうため、後段の認証サーバは TLS のクライアント認証を行えませんでした。 また、TLS を認証サーバで終端するのも難しいことがわかりました。

SDS(Secret Discovery Service)の利用

SDS は、証明書のようなシークレットを設定の中で静的に指定するのではなく、SDS API 経由で動的に配信するサービスです。 この機能について、設定例を交えて説明します。

例えば、Envoy における TLS の設定例では、以下のように証明書を指定しています。

static_resources:
  listeners:
  - name: listener_0
    address: {socket_address: {address: 127.0.0.1, port_value: 10000}}
    filter_chains:
    ...(省略)...
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          # クライアントと Envoy の間の通信に関する設定
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
          common_tls_context:
            # サーバ証明書を指定
            tls_certificates:
            - certificate_chain: {filename: "certs/servercert.pem"}
              private_key: {filename: "certs/serverkey.pem"}
            validation_context:
              # クライアント証明書の検証に使う CA 証明書を指定
              trusted_ca:
                filename: certs/cacert.pem
  clusters:
  - name: some_service
  ...(省略)...

この例では、static_resources、つまり静的な設定として証明書のパスを記述しています。

次に、SDS のドキュメントには、以下のような設定例が紹介されています。

static_resources:
  listeners:
  - name: listener_0
    ...(省略)...
    filter_chains:
    - transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
          common_tls_context:
            # サーバ証明書の設定
            tls_certificate_sds_secret_configs:
            - name: server_cert
              sds_config:
                resource_api_version: V3
                api_config_source:
                  # gRPC で設定をフェッチ
                  api_type: GRPC
                  transport_api_version: V3
                  grpc_services:
                  - envoy_grpc:
                      # SDS サーバの指定
                      cluster_name: sds_server_mtls
            # クライアント証明書を検証する際の設定
            validation_context_sds_secret_config:
              name: validation_context
              sds_config:
                resource_api_version: V3
                api_config_source:
                  # gRPC で設定をフェッチ
                  api_type: GRPC
                  transport_api_version: V3
                  grpc_services:
                  - envoy_grpc:
                      # SDS サーバの指定
                      cluster_name: sds_server_uds
  clusters:
  ...(SDS サーバの設定に続く)...

このように、Envoy に対してシークレットの設定を gRPC でフェッチするように指示し、シークレットの設定を配信するコントロールプレーンを用意することで、柔軟にシークレットの設定ができます。

しかし、調査の結果、SDS は証明書の入れ替えなどの際に、その管理を自動化する目的で使われることが多い機能で、SNI ごとにロードする証明書を切り替えるといった機能は持っていませんでした。 そのため、当初の要件を満たせず、この機能も使えないという結論に至りました。

LDS(Listener Discovery Service)の利用

LDS は、Envoy における listeners の設定を動的に配信できるサービスです。 まずは、もう一度静的な TLS の設定例を見てみます。

static_resources:
  listeners:
  - name: listener_0
    address: {socket_address: {address: 127.0.0.1, port_value: 10000}}
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        ...(省略)...
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
          common_tls_context:
            tls_certificates:
            # サーバ証明書の指定
            - certificate_chain: {filename: "certs/servercert.pem"}
              private_key: {filename: "certs/serverkey.pem"}
            validation_context:
              # クライアント証明書を検証する CA 証明書の指定
              trusted_ca:
                filename: certs/cacert.pem
...(省略)...

コメントにも記載した通り、static_resources.listeners 以下の設定で、サーバ証明書とクライアント証明書を検証する CA を指定できます。 そして、static_resources.listeners[].filter_chains[] には、filter_chain_match.server_names を指定することにより、SNI の値によって適用する設定を選択できそうだということがわかりました。 よって、SNI のバリエーションの数だけ filter を用意し、それぞれの中で使う CA を指定すれば、要件を満たせそうだと考えました。 これを実現するため、データプレーンには listeners 以下に記述されている内容を、LDS API 経由で受け取るような設定を記述します。 そして、LDS API に沿って、そのような設定を配信するようなコントロールプレーンを開発するというのが、今回の方針です。

実装

実装は以下で公開しています。コントロールプレーンの実装には go-control-plane を使いました。

github.com

注意点

今回開催したインターンでは、参加者自身に調査してもらい、コードを書いて進めてもらいました。 しかし、社内向けに書いたコードを公開するにあたって、メンターによる修正作業を行う必要がありました。 その作業の関係上、リポジトリのコミットログにはメンターのアカウント名が含まれていますが、この成果の大部分はインターン参加者によるものだということを申し添えます。

システムの構成図

以下に本システムの構成図を示します。 Envoy はコントロールプレーンから設定をフェッチしています。 その内容は以下の通りです。

  • クライアントが Envoy にクライアント証明書とともにアクセスすると、SNI に応じてクライアント証明書を検証する CA を選択し、TLS を終端する。
  • クライアント認証をパスしたリクエストは、Upstream に転送される。
  • その際、プロキシされたリクエストには、X-Forwarded-Host ヘッダが含まれる。X-Forwarded-Host ヘッダの値には、クライアントがアクセスしたときのホスト名が入る。

また、Upstream は、リクエストの X-Forwarded-Host の値を返すだけのサーバとして振る舞います。

LDS を用いたアーキテクチャ

コードの説明

リポジトリには、Envoy の設定ファイル、コントロールプレーンと Upstream のソースコード、そしてそれらをコンテナとして起動し、テストするための設定ファイルなどが含まれます。

Envoy の設定ファイル

Envoy の設定ファイルはリポジトリ内の config/envoy/envoy.yaml にあり、 go-control-plane の sample/bootstrap-xds.yaml を使いました。 内容としては、dynamic_resourceslds_config を、api_config_sourceapi_type: GRPC を指定することにより、LDS の設定を gRPC 経由で受け取れるようになっています。

コントロールプレーン

コントロールプレーンの実装は go-control-plane の example を参考にしました。 ディレクトリ構成は以下の通りです。

tree control-plane
control-plane
├── Dockerfile
├── go.mod
├── go.sum
├── logger.go
├── main
│   └── main.go
├── README.md
├── repository.go
├── resource.go
└── server.go

main/main.go はコントロールプレーンのエントリポイントで、コントロールプレーンとして振る舞う gRPC サーバを起動します。

server.go は、gRPC サーバの設定を行います。

resource.go は最も重要なファイルで、データプレーンに配信する内容を生成します。 具体的には、上で説明したような、SNI の数だけ filter を用意し、それぞれで使用する CA の証明書を指定した設定を組み立てる処理を行います。 このファイルで定義した関数がサーバから呼ばれます。

注意が必要なのは、filter 内で TLS Inspector を有効化しておかないと、SNI を解析できないという点です。 本リポジトリでは、以下の行で有効化しています。

https://github.com/cybozu-go/envoy-ca-selection-poc/blob/main/control-plane/resource.go#L248-L251

repository.go は、サーバ証明書やクライアント証明書を検証する CA の証明書を読み込みます。 このファイルで定義した関数が resource.go の関数から呼ばれます。

Upstream

upstream/main.go は、リクエストの X-Forwarded-Host ヘッダの値をレスポンスとして返すサーバを起動します。

動作確認

README.md に記載の通り、以下の手順で動作確認ができます。

$ make cert
$ make up
$ make test

make cert は、以下の証明書を発行します。

  • ルート CA 証明書
  • サーバ証明書(ルート CA から発行)
  • Apple CA 証明書(apple.example.com に対応する CA。ルート CA から発行。)
  • Banana CA 証明書(banana.example.com に対応する CA。ルート CA から発行。)
  • Apple クライアント証明書(Apple CA から発行)
  • Banana クライアント証明書(Banana CA から発行)

make up は、コントロールプレーン、データプレーン、Upstream コンテナを起動します。

make test は、curl を使って apple.example.combanana.example.com に向けて GET リクエストを送ります。 以下はその結果です。

$ make test
./scripts/test apple.example.com
* Added apple.example.com:10000:127.0.0.1 to DNS cache
* Hostname apple.example.com was found in DNS cache
*   Trying 127.0.0.1:10000...
* TCP_NODELAY set
* Connected to apple.example.com (127.0.0.1) port 10000 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: certs/apple.example.com/ca.pem
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: C=US; ST=CA; L=San Francisco; CN=example.com
*  start date: Oct 24 05:16:00 2023 GMT
*  expire date: Oct 23 05:16:00 2024 GMT
*  subjectAltName: host "apple.example.com" matched cert's "*.example.com"
*  issuer: C=US; ST=CA; L=San Francisco; CN=Root CA
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host: apple.example.com:10000
> User-Agent: curl/7.68.0
> Accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< date: Tue, 24 Oct 2023 05:25:12 GMT
< content-length: 23
< content-type: text/plain; charset=utf-8
< x-envoy-upstream-service-time: 0
< server: envoy
< 
* Connection #0 to host apple.example.com left intact
apple.example.com:10000

./scripts/test banana.example.com
* Added banana.example.com:10000:127.0.0.1 to DNS cache
* Hostname banana.example.com was found in DNS cache
*   Trying 127.0.0.1:10000...
* TCP_NODELAY set
* Connected to banana.example.com (127.0.0.1) port 10000 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: certs/banana.example.com/ca.pem
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
*  subject: C=US; ST=CA; L=San Francisco; CN=example.com
*  start date: Oct 24 05:16:00 2023 GMT
*  expire date: Oct 23 05:16:00 2024 GMT
*  subjectAltName: host "banana.example.com" matched cert's "*.example.com"
*  issuer: C=US; ST=CA; L=San Francisco; CN=Root CA
*  SSL certificate verify ok.
> GET / HTTP/1.1
> Host: banana.example.com:10000
> User-Agent: curl/7.68.0
> Accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< date: Tue, 24 Oct 2023 05:25:12 GMT
< content-length: 24
< content-type: text/plain; charset=utf-8
< x-envoy-upstream-service-time: 0
< server: envoy
< 
* Connection #0 to host banana.example.com left intact
banana.example.com:10000

この結果から、apple.example.com と banana.example.com への TLS 接続におけるクライアント認証が成功していて、Upstream からのレスポンスである、クライアントがアクセスしたホスト名が表示されていることがわかります。

今後取り組みたいこと

時間の都合上、インターン期間中に Envoy の性能検証までは実施できませんでした。 運用環境では、SNI のバリエーションは非常に多数であり、それと同じだけの filter_chains を設定しても高速に動作するのか、また、設定のフェッチにどれくらいの時間がかかるのかなどについても調べる必要があります。 その他、Envoy が TLS アラートを返す際に、その挙動をカスタマイズできるのか、などについてもまだ調べられていません。

今後、クライアント認証の基盤に使うソフトウェアを選定する際には、今回のインターンで得られた知見に加え、この辺りの情報を揃えた上で、本番環境に導入できるかどうか検討する予定です。

最後に

今回のプラットフォームコースのインターンでは、Envoy を用いた、TLS のクライアント認証の際に使用する認証局を動的に選択する方法の検証というテーマで実施しました。 インターン参加者自身で高速に試行錯誤のサイクルを回してくれたことによって、インターン期間中に PoC が動作するところまで進められました。 この成果により、現行基盤上のクライアント認証を行うコンポーネントを Kubernetes 基盤に移行できる可能性があるとわかり、また、Envoy に関する知見を深められました。 このインターンが、参加者にとっても有意義なものになっていればとても嬉しいです。

最後になりますが、Cloud Platform 部では一緒に働いてくれる方を募集中です。 以下の募集要項を見て興味を持った方はぜひご応募ください。

cybozu.co.jp

*1:TLS の拡張で、TLS のコネクション確立時に接続先のサーバ名をクライアントからサーバに伝えられます。

*2:Nginx でも、SNI ごとにクライアント証明書を検証する CA を使い分けられます。しかし、SNI のバリエーションが増えた時、それに対応する CA をロードするには再起動が必要です。私たちのユースケースでは頻繁に SNI のバリエーションが増えることが予想されるため、そのたびに Nginx の再起動を行うのは現実的でないと判断しました。

サイボウズサマーインターン 2023 報告 〜 Android コース 〜

こんにちは!kintoneチーム所属のAndroidエンジニア、トニオ(@tonionagauzzi)です。
今回は、サイボウズのインターンシップにおけるAndroidアプリ開発コースについて紹介したいと思います!

概要

サイボウズでは毎年サマーインターンシップを開催しています。2023年度のサマーインターンシップでも、さまざまなコースを用意しました。
この記事では、Androidアプリ開発の2コースについて紹介します。

インターンの内容

Androidアプリ開発は、2種類のコースを用意しました。

Android 1週間コースには2名、Android 2週間コースには3名のインターン生にご参加いただきました。
インターン生にはリモートワークで実際のプロジェクトに参加することで、実践的な経験を積んでいただきました。

Android 1週間コース

まずはkintoneチームで行われたAndroid 1週間コースの内容です。

インターン中の開発の流れ

kintone Androidチームの業務は、主にモブプログラミング形式(以下、モブ)で行っています。
今回のインターンでは、インターン生2名と社員3名の合計5名で、Zoomを用いたモブを行いました。

インターン中のスケジュール

大まかなタイムスケジュールは以下の通りでした。

Android1週間コース時間割
Android1週間コース時間割

ミーティングへの参加

kintone Androidの開発は、1週間スプリントのスクラムベースで実施しており、インターン中もスクラム開発は行われています。
インターン生には普段のスクラムイベントのうち、朝会(デイリースクラム)に出席していただきました。

また、kintone開発チーム全体で実施しているスプリントレビューも見学していただきました。
インターン生が参加したスプリントレビューは、各チームによる成果の説明のあと、プロジェクトマネージャーから次の3か月の開発計画が紹介されるタイミングでもありました。
その日の夕会で感じたことを聞かせていただきました。
そこで得られた感想は以下の通りです。

  • 成果を褒めながら進められていたのが良かった
    • もっと業務連絡っぽいと予想していた
  • 開発計画の説明内容が、とても考えられたもので良いなと思った

社内イベントへの参加

メンターや同じチームのメンバーだけでなく、チーム外の人との交流の機会も設けました。
kintoneのiOSエンジニア、QAエンジニア、PMとの雑談会、他プロダクトも交えたAndroidエンジニア同士の雑談会、懇親会、そして青野社長との雑談会も行われました。
リモートワークにもかかわらず、コミュニケーションを多くとって大切にしているという印象を持っていただけたのではないかと思います。

取り組んだタスク

私たちがインターンで取り組んだバックログアイテムは、ファイル添付時に写真選択ツールを利用して画像や動画を添付できるようにする、というものです。

背景

kintoneは、サイボウズが提供する業務アプリ開発プラットフォームで、ノーコードでお客様の用途に合わせた業務アプリの作成が可能です。
その機能の1つとして、kintone内で作成したアプリやスペースなどに、写真や動画、ファイルを添付する機能があります。
添付操作の際、モバイルアプリでは以下のようなボトムシートが表示され、どの方法で添付するかを選択できます。

既存の添付ボトムシート
既存の添付ボトムシート

  • カメラ…カメラが起動し、その場で撮った写真を添付する
  • カメラ(ビデオ録画)…カメラが起動し、その場で撮った動画を添付する
  • ファイル…デバイス内に保存されたファイルを選んで添付する

課題

すでに撮影済みの写真や動画を選びたい場合、以下の課題が存在していました。

  • 写真や動画なのに「ファイル」の選択肢を選ぶ必要がある
    • 一見わからず、誤ってカメラを起動して迷ってしまう
  • 「ファイル」アプリを開いてから、目的の写真や動画へスムーズに辿り着けないことがある

そのため、Androidの操作に詳しくない人には、やや使いにくい仕様になっていました。

取り組んだこと

Androidには撮影済みの写真や動画を選びやすい写真選択ツールが存在するため、その写真選択ツールをファイル添付時のボトムシートの選択肢に追加しました。
メンターによる計画時は、選択肢を3つから4つに増やすだけの、技術的には大したことない作業だと考えていました。

問題発生!

ところが、インターンが始まり着手してみると、既存のボトムシートには写真選択ツールを追加できないことが発覚しました!

Intent.createChooserに指定するEXTRA_INTENTEXTRA_INITIAL_INTENTSの制限で、規定のIntentを1つ、追加のIntentを2つまでしか設定できないからです。
すでに規定のIntentに「写真」が、追加のIntentに「動画」と「ファイル」の2つが設定済みです。前述の上限に引っかかっており、これ以上アイコンを並べられなかったのです。

実際に選択肢を実装してみても、表示が変わりませんでした。
これは想定外!となって、実現方法を再調査し、ボトムシートを自作することにしました。

自作の添付ボトムシート
自作の添付ボトムシート

インターン期間中は、その再調査からボトムシートのプロトタイプ実装までを、インターン生とメンターとの合同モブで行いました。
メンターとしては予想外の展開でしたが、これによりインターン生はよりリアルな開発の現場事情を体験することができたのではないかと思います。

インターン生の感想

インターンに参加してくださった2名のインターン生からは、以下のような感想をいただきました。

  • インターンで期待していたチーム開発だけでなく、メンターの方同士の意見の出し合いや思考のプロセスを知ることができた
  • メンターの方達とさまざまな意見を出し合い、新たな問題に対する解決のプロセスを体験することができてよかった
  • モブという学生としては馴染みのない体験によって、チームワークや自分の考えを説明する力の重要性を理解した
  • プログラムのテスト工程等を体験できなかったのは残念

ふりかえり

メンター側ではふりかえりを行い、来年に向けて以下のような意見が寄せられました。

  • PMやQAなど、インターンで関わらない人たちの普段の業務内容も質問してもらえたので、有意義だった
  • 分報を書く暇もなかったので、もう少し自由時間を設けてもよい
  • 次はテストの話もちゃんと説明したい
  • インターン生はドライバーだったので、慣れてきた4日目くらいにナビゲーターの経験ができるとよかった
    • ただし、4日目は締め切り間近で余裕がないことも考えられる

まとめ

kintone Androidチームのインターンでは、モブ形式で開発を行い、スクラムベースの1週間スプリントを実施しました。
インターン生には朝会やスプリントレビューに参加していただき、普段の開発業務を経験しながら、社内イベントも楽しんでいただけたかなと思います。
取り組んだタスクは写真選択ツールの追加でしたが、予想外の制限によりボトムシートを自作することになりました。
インターン生にはチーム開発や意見の出し合いを経験していただけたと思いますし、メンター側も有意義なふりかえりを行いました。

Android 2週間コース

続いて、サイボウズOffice Androidチーム(以下、Office Android)で行われたAndroid 2週間コースの内容です。

インターン中の開発の流れ

Office Androidの業務も、主にモブで行っています。
そのため、今回のインターンではインターン生3名と社員3名の合計6名で、Zoomを用いたモブを行いました。

インターン中のスケジュール

大まかなタイムスケジュールは以下の通りでした。

Android2週間コース時間割 1週目
Android2週間コース時間割 1週目

Android2週間コース時間割 2週目
Android2週間コース時間割 2週目

ミーティングへの参加

Office Androidの開発でも、インターン生には朝会(デイリースクラム)に出席していただきました。
さらに実際のスプリントプランニングやスプリントレビュー、見積もりなどを行うリファインメントも体験していただきました。

社内イベントへの参加

こちらのチームでも他プロダクトも交えたAndroidエンジニアとの雑談会、懇親会、そして青野社長との雑談会を行いました。

ハッカソン

Android2週間コース独自のイベントとして、ハッカソンを開催しました。 目的は、インターン生にモブやJetpack Compose(以下、Compose)を使った開発に慣れていただくためです。
ハッカソンの中で、2種類の課題をこなしていただきました。

  1. Composeの標準Componentを各自触ってみよう!(難易度:低)
  2. 標準にないComponentを、モブで1から作ってみよう!(難易度:高)

以下がインターン生に作ってもらった自作Componentです!

リストの1項目を構成するListTile!
リストの1項目を構成するListTile!

押すと拡張するExpansionTile!
押すと拡張するExpansionTile!

Tinderのようにリストアイテムをスタック表示するStackedListPreview!
Tinderのようにリストアイテムをスタック表示するStackedListPreview!

ハッカソンを実施してよかった点は、モブやComposeに早い段階で慣れるという目的を達成できたことでした。
一方、改善点として、ナビゲーターの役割分担が挙がりました。2人以上がナビゲーターを担当していると、誰が指示を出すかが明確でないことがありました。今回はそれに気づいた時点で、指示を出さない人はハウスキーパーに徹してもらうようにしました。
来年はより明確な役割分担を行い、チーム全体が円滑にコミュニケーションを取れるように改善していきたいと考えています。

取り組んだタスク

2週間コースのインターンでは、Office Androidの実際のスケジュールを改修するバックログに取り組みました。
取り組んだバックログアイテムは、以下の内容です。

  • 予定にメモを登録/変更できるようにする
  • 時刻を表示しない終日予定を登録/削除できるようにする

背景

Officeでは、最近モバイル版アプリ「サイボウズOffice新着通知」を「サイボウズOffice」へ名称変更し、リニューアルしました。
モバイルからの操作を起点とする機能を追加し、業務をいつでもどこでも進められる環境を目指しています。
基本的な機能が揃ったことを受け、アプリ名を「サイボウズOffice」に変更しました。
今後も、働き方の多様化に対応し、モバイルひとつで業務を迅速に進められるツールとして、利用者の課題に対する最適なソリューションを提供し続ける予定です。

詳細は以下のリンクをご覧ください。
https://topics.cybozu.co.jp/news/2023/03/06-18409.html

Officeの持つさまざまなグループウェア機能の1つに、予定を登録してチームや組織で共有するスケジュール機能があります。

課題

開始時刻と終了時刻が明確でない予定(たとえば、休みや終日外出など)を登録したくても、モバイルアプリでは開始日時と終了日時の入力欄に必ず時刻を入力する必要がありました。 また、訪問先などの詳細をメモとして登録したくても、予定にメモを添付する機能はありませんでした。

取り組んだこと

インターン生が参加したスプリントでは、スケジュールを登録する機能に対し、時刻情報の付かない日付だけの予定を登録できるようにしたり、予定にメモを登録したり後から変更できるようにしました。
インターン生にはモブに参加し、Composeを用いて既存の予定作成画面と予定編集画面に対し、日付だけの予定に対応していただいたり、メモの入力欄を追加していただきました。

日付だけの予定が登録可能になった!
日付だけの予定が登録可能になった!

メモの入力が可能になった!
メモの入力が可能になった!

インターン生の感想

インターンに参加してくださった3名のインターン生からは、以下のような感想をいただきました。

  • モブやスクラム開発、チームの雰囲気など期待していた以上の経験ができた
  • モブやスクラム開発の知見を教えていただき、勉強になった
  • インターンを経て、サイボウズという会社に対するイメージが変わり、フランクでフラットな職場であると実感した
  • インターン生のみが交流する場があるとよかった。同世代のインターン生が普段どのようなことを行っているのか知りたいと思った

ふりかえり

メンター側ではふりかえりを行い、来年に向けて以下のような意見が寄せられました。

  • 1スプリントのスクラムイベントをほぼすべて体験してもらえてよかった
  • どのイベントを見学してもらうか、見学しないイベントにメンターの誰が行くかを直前に決めていた
    • あらかじめ決めておいたほうがよかった
  • インターン用のプロダクトバックログアイテムがもう少し難しくてもよかった
  • カメラONでやったほうが、表情がわかるのでよかったかもしれない
  • インターン生のみの雑談会、インターン中のランチ会で交流を増やしてもよかったかもしれない

まとめ

Android2週間コースでは、Office Androidチームに参加してモブを行い、スクラムの各イベントにも参加していただきました。
また、Composeに慣れることを目的としたハッカソンを開催し、Office Androidのスケジュール改修にも取り組んでいただきました。
インターン生からはモブやスクラム開発の経験を高く評価していただけた一方で、インターン生同士の交流の場があると良いとの意見もいただきました。
メンター側では、ツールの早期導入や役割分担の明確化、インターン生の交流機会の増加などの改善点を見つけ出しました。

おわりに

インターンシップのはじめは新しいことばかりで大変だったかもしれませんが、皆さんが自律的にアイデアを出し、積極的に取り組む姿を見て、私たちは大いに刺激を受けました。
この経験が皆さんの技術的な成長だけでなく、チームでのコミュニケーションの経験にも繋がったとしたら、私たちはとても嬉しいです。
そして、この経験が皆さんの今後のキャリアにとって大きなプラスになることを心から願っています。
この短期間で見せてくれた皆さんの成長と努力に感謝し、今後のさらなる活躍を期待しています!

サイボウズサマーインターン 2023 報告 〜 ストレージコース

こんにちは、ストレージチームの林、大神です。

サイボウズでは毎年サマーインターンシップを開催しています。2023年度のサマーインターンシップもさまざまなコースを用意しました。本記事では8/28(月)~9/8(金)の10日間にわたって開催されたストレージコースでの取り組みについて紹介します。

ストレージコースでは昨年までと趣向を変えて、ストレージチームの普段の業務にほぼそのまま参加してもらい、よりリアルな業務を体験していただきました。 今回のインターンシップの参加者は1名でした。

一日の流れ

ストレージチームの業務は主にモブプログラミング形式で行っています。そのため、今回のインターンでもインターン生1名と社員2名の合計3名で Zoom を用いたリモートモブプログラミング(以下、モブ)を行いました。

ただし、ストレージチームでは一日中モブを行っているわけではなく、モブの開始前と終了後のそれぞれ一時間は個人で好きに使ってよい時間としています。 この時間では気になっている技術の勉強をしたり、コードベースの改善活動を行ったりと、 チームメンバーがそれぞれ自分で考えて行動します。

今回のインターンでもこの時間はいつも通り確保し、 インターン生には知識の整理のための時間としてもらったり、モブでのタスクとは別に個人用のタスクに取り組んでもらったりしました。

インターン全体のタイムスケジュール

  • 8/28(月)~8/29(火):人事オリエンテーション、チーム紹介、講義
    • Kubernetes、Rook/Ceph、およびストレージチームが開発している新 Blob サービス(後述)等について説明しました
  • 8/29(火)~9/7(木):モブ、各種ミーティングへの参加、個人タスク
    • タスクを実施する時間の他に、実際にストレージチームが行っている各種ミーティングにもほぼそのまま参加してもらい、リアルな業務の雰囲気を味わっていただきました
  • 9/8(金):成果発表、懇親会

ミーティングへの参加

リアルな業務を体験して頂くという趣旨のもと、今回のインターン期間中にストレージチームが開催したミーティングにはインターン生にもほぼ全て参加していただきました。 Daily meeting では個人で取り組んで頂いたタスクの軽い進捗報告をお願いしました。

また、インターン期間中に発生した障害の振り返り会にも参加して頂きました。 障害の発生は偶然だったのですが、ウェブサービスを提供する企業が障害にどのように向き合っているかを知る良いきっかけになっていれば嬉しく思います。

取り組んだタスク

今回は予定よりも進捗が良かったため、最終的に4つのタスクを実施しました。 ここでは下記の2つをピックアップしてご紹介します。

  • Blob サービスの分散トレーシング対応
    • モブで実施
  • ブロックストレージ専用の Ceph クラスタにオブジェクトストレージ関連の不要なプールが作成されないようにする
    • 個人タスクとして実施

Blob サービスの分散トレーシング対応

ストレージチームでは Neco 基盤向けに新しい Blob サービスを開発しています。これは cybozu.com で提供している各種アプリケーションから使用される内部的なサービスとなっています。 今回のタスクではこの新 Blob サービスの分散トレーシング対応を行っていただきました。

対応した社員2名も分散トレーシングにはあまり精通していなかったのですが、こちらのブログなどを見ながら対応を行い、 最終的には以下のように無事に Grafana 上で分散トレーシングの様子を見ることができました。

新 Blob サービスの分散トレーシング
新 Blob サービスの分散トレーシング

ブロックストレージ専用の Ceph クラスタにオブジェクトストレージ関連の不要なプールが作成されないようにする

ストレージチームでは Neco 基盤向けに複数の Ceph クラスタを運用しています。 Ceph にはブロックストレージ、ファイルストレージ、およびオブジェクトストレージとして使用するための3種類のインターフェイスが存在します。 Neco 基盤ではこのうちブロックストレージとオブジェクトストレージとして Ceph を活用しており、 ブロックストレージとオブジェクトストレージで Ceph クラスタを分けて運用しています。

Ceph を運用していく中で、Ceph の何らかの情報をメトリクスとして取得したいと思っても、 Ceph がそれを出力してくれていないということがたまにあります。 ストレージチームではそのようなときにのために、 定期的に Ceph のコマンドを実行してその結果から必要な情報を抽出してメトリクスとして出力するツールを開発しています。

実はこのツールから実行されるコマンドのうちの1つに副作用があり、 オブジェクトストレージとしての機能を提供していない Ceph クラスタに対してコマンドを実行すると、 オブジェクトストレージ関連のデフォルトプールが作成されてしまうことが分かっていました。 このようなプールが作成されることは意図していないため、 このタスクではそれを防ぐための開発を行いました。

Ceph の upstream に issue を立ててこの挙動自体を改善することも考えられましたが、 今回はオブジェクトストレージ関連の情報を参照するために実行するコマンドがトリガーとなっており、 そもそもブロックストレージのみを提供する Ceph クラスターに対して実行する必要がありませんでした。 そこで、コマンドライン引数を1つ増やして、 オブジェクトストレージ関連の情報を収集するか選択できるようにして、 無駄にオブジェクトストレージ関連の情報収集コマンドが実行されないようにすることで解決しました。

まとめ

今回のインターンシップでは実際の業務をほぼそのまま体験していただくという都合上、 インターン生にはある程度技術的にキャッチアップしていただく必要がありました。また、チームでの開発になるのでコミュニケーションをうまく取りながら進める必要があるなど、 インターン生にとってそこそこハードな側面もあったかと思います。 ですが、そんなこちらの心配を乗り越えて、最終的には想定以上にタスクを進めていただきました。 今回の経験が今後のインターン生のキャリアにとってプラスになることを願っています。

最後になりますが、ストレージチームでは一緒に働いてくれる方を募集中です。下記の募集要項を見て興味を持った方はぜひご応募ください。

cybozu.co.jp

大規模Kubernetesクラスタにおけるコンテナレジストリの高速化

こんにちは。サマーインターンシップ2023のKubernetes基盤開発コースに参加した、高橋 (TAK848) と花田 (hanapedia) です。

Necoチーム では現在コンテナレジストリの可用性を高めるため、コンテナレジストリミラーをクラスタ内にデプロイしています。 しかし、クラスタが数百台程度まで大きくなり、レジストリミラーからコンテナイメージを同時に大量にPullするようになりました。 そのため、イメージの取得に数十分もの時間がかかってしまう問題が起きました。

この問題に対処するため、Spegel というステートレスなレジストリミラーの動作検証を行い、PodのイメージのPullのタイミングを制御するカスタムコントローラー cat-gate を開発・検証しました。 その成果を紹介します。

課題

KubernetesではPodを起動する際、コンテナイメージを取得する必要があります。Podがアサインされたノードにそのコンテナイメージがない場合、upstreamのコンテナレジストリからイメージをPullします。Kubernetesを運用する上で重要な役割を担うコンテナレジストリですが、

  • 単一障害点 (Single Point of Failure; SPOF) になり得る
  • 多数のPodを起動した際に大量のトラフィックが発生する
  • サービスによってはrate limitなどが設けられている

などといった課題が存在します。

外部のContainer Registryとの通信が止まるとコンテナイメージがダウンロードできない

Necoにおける解決策

Necoチームでは現在、セルフホストコンテナレジストリをレジストリミラーとして用い、pull-through cacheとして利用することで上記の課題に対応しています。

blog.cybozu.io

こうすることで、一度Pullしたイメージがレジストリミラーにキャッシュされるため、以下のようなメリットが得られます。

  • upstreamの障害の影響を受けにくい(初回のPullのみ影響を受ける)
  • upstreamとの通信量を減らせる
  • イメージのPullにかかる時間を短縮できる

現在の課題

上の方法で、upstreamのレジストリに由来する問題は緩和されました。しかし、Kubernetesクラスタの規模が大きくなるにつれ、レジストリミラーならではの課題が浮き彫りになりました。

例えば、クラスタ内でDeploymentやDaemonSetを作成すると、同時に大量のイメージ取得が発生するため、レジストリミラーに負荷が集中し、イメージの取得に数十分かかるケースが出てきました。

いくつかの解決策を検討しましたが、以下の理由でNecoでの採用は見送りました。

  • DragonflyKraken などといった分散型コンテナレジストリを用いてセルフホストレジストリをスケールする
    • DragonflyはバックエンドとしてDBやキャッシュを利用するため、運用すべきコンポーネントが増える
    • Krakenはpull-through cacheをサポートしていない
  • IPFS をcontainerdのバックエンドに指定し、ノード間でコンテナイメージを共有する
    • コンテナイメージの取得方法やIPFSの運用が複雑になる

そこで、ステートレスかつKubernetesネイティブなSpegelというOSSのレジストリミラーに白羽の矢が立ちました。

Spegel

SpegelはスウェーデンのXenit社が主体となって開発している、ステートレスな分散レジストリミラーです。OCIに準拠していますが、現在はcontainerdのみに対応しています。

アーキテクチャ

Kubernetesクラスタの各ノードのcontainerdが保持しているイメージを共有することで、Spegel自体がステートを持たない設計になっています。

containerdがupstreamからダウンロードしたイメージのdigestをSpegelインスタンス間で共有する

各ノード上のSpegelは、自分のノードのcontainerdが持っているイメージのdigestをP2Pで他のノードに広告しています。

Podが新しく作成されたとき、必要なイメージのdigestが他のノードから広告されていれば、そのノードにコンテナイメージを取りに行きます。広告されていなければ、upstreamのレジストリに取りに行きます。 取得されたコンテナイメージはcontainerdのキャッシュに保持され、以降他のノードから利用できるようになります。

デプロイ構成

SpegelはDaemonSetとして各ノードにデプロイします。 ノード上のcontainerdと通信するため、以下の設定が必要です。

  • containerdのsocketをSpegelのPodにHostPathでマウントする
  • Spegelのエンドポイントをローカル、リモートのcontainerdに公開する

コンテナイメージ取得の流れ

また、containerdの設定を変更し、ミラーを使いたいコンテナイメージのレジストリを以下の順番で解決するようにします。

  1. 同一ノードのSpegel
  2. 別のノードのSpegel
  3. コンテナレジストリ (upstream)

詳細なインストール手順はSpegelのREADMEをご確認ください。

上記の構成でSpegelをNecoのクラスタにデプロイし、パフォーマンスを計測するため以下の検証を行いました。

PodをデプロイしたときのPull速度の比較

Spegelを導入していない場合と導入した場合で、イメージのPullにかかる時間を計測しました。

まずは、あるイメージのPodをデプロイし、完了後に同じイメージのPodを別のノードにデプロイしました。

初回のデプロイ時はどのノードにもイメージが無いので、upstreamからイメージを取得します。 2回目のデプロイの際は、通常は自分のノードにイメージがないためupstreamからイメージを取得しますが、Spegelを導入すると最初のノードにPullされたイメージをSpegel経由で取得するため、高速化されます。

以下のテーブルにイメージのダウンロードにかかった時間をまとめました。 計測は6ノードの環境(以下、クラスタA)と77ノードの環境(クラスタB)で行いました。

クラスタ Spegelの有無 1ノード目 [s] 2ノード目 [s]
A(6ノード) なし 7.92 7.42
A(6ノード) あり 49.26 5.33
B(77ノード) なし 9.73 7.93
B(77ノード) あり 49.75 36.44

upstreamからイメージをPullした場合は、7-10秒ほどでダウンロードが完了しています。 Spegelを導入した場合、2ノード目でのPullは、クラスタAでは5.3秒で、upstreamから取得するより速くなりました。

クラスタBでは、2ノード目でのPullは1ノード目に比べて若干速くなっているものの、Spegelが導入されていない場合よりPullに時間がかかるようになりました。 ただし、十分な時間を空けてから以降のPodをデプロイするとクラスタAと同じような速度になることを別途確認しました。 他のノードでダウンロードしたイメージが利用可能になるまでに多少の時間がかかるようです。

また、いずれのクラスタでもSpegelを導入すると、初回のPullに数十秒ほどかかるようになりました。 動作を確認すると、レジストリミラーへの問い合わせ中にタイムアウトが発生していることが分かりました。 ただし、2つ目以降のPodの起動が速くなるため、同一のイメージを繰り返し使った場合、全体としてイメージの取得は速くなります。

DaemonSetをデプロイしたときのPull速度の比較

次に、より実際のアプリケーションをデプロイした場合に近いパフォーマンスを計測するため、DaemonSetをデプロイしました。

Spegelを導入していない場合と導入した場合それぞれで、DaemonSetの初回デプロイ時と、1台ずつローリングアップデートした時の所要時間を比較しました。

クラスタA(6ノード)

各PodのイメージのPullにかかった時間は以下の通りです。

起動した順番 条件1 条件2 条件3 条件4
Spegelなし
初回デプロイ
Spegelなし
アップデート
Spegelあり
初回デプロイ
Spegelあり
アップデート
1 7.33 8.04 48.31 49.27
2 7.16 7.43 47.76 5.28
3 7.24 8.07 48.80 5.35
4 7.47 7.08 48.62 5.35
5 7.56 7.04 48.62 5.32
6 7.52 7.05 48.36 5.28

ワークロードリソースを最初にデプロイする時は、全てのPodが同時に起動し、イメージのPullを開始します。 そのため、Spegelの有無に関わらず、各ノードそれぞれがupstreamからイメージをPullします。

ローリングアップデート時も、Spegelが無い時は各ノードからイメージのPullが行われます。それぞれが約7秒かかり、全体で約50秒かかりました。

一方Spegel導入時は、1回目のPullはCache Hitせずupstreamから取得していて、約49秒かかりました。 2台目以降は1台目がダウンロードしたイメージがCache Hitし、毎回5秒ほどでPullができています。 upstreamからの取得が1度初回に行われ、ここでは時間はかかりますが、2回目以降はクラスタ内のノードから高速に取得ができています。

クラスタB(77ノード)

クラスタB(77ノード)では、DaemonSetのマニフェストを設定してから全てのPodがReadyになるまでの時間を計測しました。

Spegel の有無 初回デプロイ [s] ローリングアップデート [s]
なし 27.62 839.52
あり 66.64 916.87

各Podのコンテナイメージをダウンロードしている時刻

初回デプロイ時にイメージをPullする時間は、Spegelを導入しない場合、Podの数が1つの場合(約8秒)より時間がかかって約28秒となりました。 これは77ノードからupstreamに同時にイメージのPullをリクエストしているため、通信が遅くなっていると考えられます。

Spegel導入してローリングアップデートを行った場合、Pod単体での検証と同様に、最初のPod数個分のイメージのPullで時間がかかりました。 ただし、後続のPodはキャッシュからイメージを取得するためSpegelなしの場合(約7秒)より速く、それぞれ5秒ほどで完了し合計900秒程度かかりました。

また、本筋とはあまり関係がありませんが、イメージをノードにダウンロードするのにかかる時間が47秒 > 36秒 > 15 秒…と段階的に下がっていく現象も確認できました。

Spegel の導入で見えた課題

実際にクラスタを運用する際は、DeploymentやDaemonSetなどで複数のPodをまとめてデプロイしたり、更新したりすることが多いと思います。 ただ、上記で見た通り、大規模クラスタでSpegelを利用するには以下のような課題があることが分かりました。

  1. DaemonSetなどを使って大量のPodを同時にデプロイするとキャッシュが効かない
  2. レジストリミラーへの問い合わせのタイムアウト待ちに時間がかかり、ローリングアップデートが遅くなってしまう場合がある

我々はこの2つの課題に対し、

  1. Podのスケジューリングのタイミングを調整するカスタムコントローラの作成
  2. Spegelのパラメータ調整

という2つの手段で対策を行いました。 以下で内容を説明していきます。

cat-gate

cat-gateは、Kubernetes 1.26で導入された Pod Scheduling Readiness という機能を使って、Podのスケジューリングのタイミングを調整するカスタムコントローラです。

Pod Scheduling Readiness

通常、Podは作成直後にkube-schedulerによってノードにスケジュールされ、利用するコンテナイメージのダウンロードが始まります。 しかし、以下のように schedulingGates を指定することでノードへのスケジュールを保留させることができます。

apiVersion: v1
kind: Pod
metadata:
  name: sample
spec:
  schedulingGates:
  - name: my-scheduling-gate
  containers:
  - name: ubuntu
    image: ubuntu:22.04

設計

cat-gate は、以下の方法でPodのスケジューリングのタイミングを調整します。

  1. MutatingWebhook で全てのPodに以下の処理を行う
    1. scheduling gateの付与
    2. 必要なイメージの一覧のハッシュ値を計算し、Podのannotationに付与
  2. Podの Reconciler で以下の処理を行う
    1. Reconcilerに入ってきたPodと同じハッシュ値を持つPodの一覧を作成し、「コンテナイメージをダウンロードしているPodの数 (X)」と「コンテナイメージを持っているノードの数 (Y)」を計算する。ただしYは「コンテナのステータスがRunningまたはTerminatedになっているPodが乗っているノードの数」で代用する
    2. X < Y * 2 であれば、Reconcile対象のPodのscheduling gateを外す

Node 1-4に起動中のPod(と必要なイメージ)があり、Node 5-12にイメージをダウンロードしている様子

この設計により、たとえば4台のノードがコンテナイメージを持っていれば、Pod 8個までscheduling gateを外すことができます。 各ノードは平均して2並列までしかイメージをリクエストされないため、特定のレジストリミラーのインスタンスに負荷が集中する問題を回避できます。

mirror-resolve-timeoutの設定

先述の実験で各種ログを確認したところ、Spegelが他のノードのインスタンスからの応答を長時間待っているために最初のPodのデプロイが遅くなることが分かりました。 Necoのクラスタのノードは高速なネットワークで相互接続されているため、Spegelの --mirror-resolve-timeout をデフォルトの5秒から2秒に切り詰めました。

再測定

クラスタB(77ノード)にcat-gateをデプロイし、--mirror-resolve-timeout を設定して再度測定を行いました。 結果を表にまとめると以下のようになりました。全てのデプロイが完了するまでに要した時間は105秒ほどでした。

起動した順番 Pull の開始時刻 [s] Pull の所要時間 [s]
1 0.00 25.72
2-3(計2台) 23.50 20.25
4-6(計3台) 45.00 19.39
7-9(計3台) 53.00 19.32
10-27(計18台) 61.28 6.21
28-77(計50台) 73.28 7.08

イメージをPullする経過をガントチャートで視覚的に表すと以下のようになりました。

cat-gate導入時のPodのイメージのダウンロード時刻

最初の数台は、Spegel経由でのPullに多少時間がかかっているものの、後半に進むにつれてまとめてコンテナイメージを受け渡しできるようになり、デプロイが高速化されました。 ノード数を増やしても所要時間は対数でしか増えず、イメージのPullはごく少ない回数で済むという二重の恩恵を得ることができました。

ローリングアップデートの挙動は大きな変更がないため検証を省きますが、初回のダウンロードにかかる時間が改善されるため、更新が完了するまでの時間が短縮されます。

おわりに

本記事では、ステートレスな分散レジストリミラーであるSpegelと、Podのスケジューリングのタイミングを調整するcat-gateを紹介し、実際のクラスタに適用した際のパフォーマンスを解説しました。 今回はテスト用のイメージにのみ設定を行ったため、今後はクラスタ内のなるべく多くのイメージに対してレジストリミラーを設定し、動作を確認していきます。

今回のインターンでは、最初はKubernetesの基礎から深い内容、コンテナランタイム、Kubebuilderについてなど、様々なテーマでハンズオンや勉強会がありました。 それらをモブプログラミングでアウトプットしていく形式で、とてもやりやすかったですし、知識の定着や実践的な検証・深掘りがたくさんできました。 その上で、コントローラーを実装し、OSSとして公開できて、非常に充実した3週間でした! このままの勢いで、自宅にKubernetesクラスタを組んでみたいな、と思っています。

一方で、今回の期間で解決できなかった部分もありました。 Spegelを介した際の初回のイメージPullにかかる時間の長さや、ノード数が増えた際に、段階的にイメージPullにかかる時間が変わっていく現象などの原因は詳しく解明することができませんでした。 なので今後、Spegelのソースコードを読んでみようと思います。

サイボウズサマーインターン2022 プロダクトセキュリティコース 開催報告

こんにちは!Cy-PSIRTの田口です。

本記事はサマーインターンシップ2022 プロダクトセキュリティコースの開催報告です。
今年は、8月と9月に全2タームでインターンシップを開催しました。 去年に続き、今年もフルリモートで実施しました。

概要

プロダクトセキュリティコースは、第1タームを8月1日〜8月4日、第2タームを9月5日〜9月8日の4日間で開催し、各タームそれぞれ4名の学生にご参加いただきました。
インターンでは、Cy-PSIRTが普段行っている業務をもとにコンテンツを用意しています。今回インターン生の皆さんには、主に以下の内容について体験していただきました。

  • 製品理解・ハンズオン
  • 脆弱性検証
  • 脆弱性評価
  • 外部通報対応

Cy-PSIRTの具体的な業務内容については以下の記事で紹介しています。

blog.cybozu.io

製品理解・ハンズオン ✏️

はじめに、サイボウズ製品や脆弱性について学んでいただきました。
Cy-PSIRTの業務を行う上で、製品への理解は大切です。そのため、製品の基本的な機能や仕様、操作方法について簡単に知っていただきました。
その後、Webアプリケーションの脆弱性と脅威について学び、ハンズオンを行なってもらいました。 ハンズオンでは、過去サイボウズ製品に存在していた脆弱性(改修済み)を再現させ、脆弱性の挙動を確認していただきました。
皆さん、手を動かしながら製品機能や脆弱性について理解を深めていただけたようでした。

脆弱性検証 🔎

脆弱性検証の時間では、普段の業務の流れと同様に以下を順番に行なっていただきました。

  • 要件確認
  • 試験仕様書の作成
  • 検証

要件確認

まずはじめに、要件確認を行いました。 要件確認とは、新規の機能実装や不具合改修、その他製品の変更など、開発チームが実施した要件を確認し、脆弱性検証の要否を判断する業務です。 要件の実例を挙げながら、脆弱性検証が必要となる判断基準や考え方を学んでいただきました。 今回は演習として、cybozu.com共通管理、kintoneGaroonに対しての要件を想定し、皆さんに脆弱性検証の要否と検証観点を考えてもらいました。

要件確認の様子(第1ターム)
要件確認の様子(第1ターム)

試験仕様書の作成

要件確認を経て、脆弱性検証が必要であると判断した要件について試験仕様書を作成します。 検証対象のリクエストについて、パラメータや権限、その他検証観点を洗い出しながらkintoneアプリに書き出していきます。 試験仕様書を作った経験がない方も多かったかと思いますが、皆さん網羅的に検証観点を考察できており素晴らしかったです!

検証

試験仕様書を作成したら、いよいよ検証に入ります。作成した試験仕様書の内容をもとに、Burp Suiteを使いながら一つずつ検証を行います。 インターン生からは、網羅的に検証することの大切さが学びになったとの感想をいただきました。 製品のセキュリティ品質を担保するという面で、PSIRTならではの検証の仕方や考え方を学んでいただく機会になったかなと思います。

ランチ 🍖

2日目のお昼はCy-PSIRTのメンバーとインターン生でランチをしました。 一緒にご飯を食べながら気軽に雑談することで、インターン生のことをより知ることができました。 インターン生の皆さんも、チームの雰囲気を知る機会になったのではないでしょうか。

ランチの様子(第1ターム)
ランチの様子(第1ターム)
ランチの様子(第2ターム)
ランチの様子(第2ターム)

脆弱性評価 🤔

続いては、脆弱性評価の時間です。サイボウズでは、脆弱性の深刻度をスコアリングする手法の一つであるCVSSv3を用いて脆弱性評価を行なっています。インターン生の皆さんには、CVSSv3の考え方やスコアの付け方を学んでいただき、過去サイボウズ製品に存在していた脆弱性を評価してもらいました。

脆弱性評価の様子(第2ターム)
脆弱性評価の様子(第2ターム)

インターン生の皆さんは、脆弱性を評価した経験はあまりないかと思いますが、あらゆる脅威や攻撃シナリオを想定し根拠を持ってスコアリングすることができていました。
後半では、算出したスコアやどのような考え方をしたのかお互いに共有する時間を設けました。 他の人のスコアを見て、評価は想定するシナリオによって異なるということを感じていただけたかと思います。Cy-PSIRT内でも、各評価項目の考え方やスコアの付け方について議論になることがよくあります。他の人の意見を聞いたり議論をしたりすることで、より公正な評価を行うことができます。
この時間を通して、スコアをつけることの難しさや脅威の考え方など脆弱性評価について新たな学びを得る機会になっていれば嬉しいです!

外部通報対応 📢

最後は外部通報対応の時間です。この時間では、サイボウズが運営している脆弱性報奨金制度の対応業務を体験していただきました。外部通報対応は、製品理解、脆弱性の再現確認や検証、脆弱性評価など、これまでの時間で体験した業務の理解が必要になってきます。 過去に外部から報告された内容を用い、「トリアージ」「再現確認」「評価」の一連の流れを体験していただきました。
Cy-PSIRTメンバーで演習のレビューを行いましたが、皆さんこれまで学んだ内容を活かしながら的確に対応できており素晴らしかったです! インターン生からも、レビューがOKで返ってきた時インターンでの学びが認められた感じがして嬉しかったとの声をいただきました。
実際にあった報告を手を動かして対応してみることで、脆弱性報奨金制度の運営側を知っていただくよい機会になったかなと思います。

成果報告会 🏆

最終日の夕方は、成果報告会を実施しました。今年は例年と異なり、kintoneアプリを使った形式で実施しました。 インターン生の皆さんには、「やったこと」「学び、気づき」「やってみたいこと」「感想」をベースに、各コンテンツについて事前に振り返りを記入していただきました。 成果報告会では、その振り返りの内容をもとに各コンテンツについての学びや感想を共有していただきました。

成果報告会の様子(第2ターム)
成果報告会の様子(第2ターム)

4日間のインターンを通して多くの学びや気づきがあったようで、Cy-PSIRT一同大変嬉しかったです!

懇親会🍺

最終日の夜は懇親会を行いました。去年に続き、Cy-PSIRTメンバーに聞いてみたいことを事前に登録していただき、その内容をもとに雑談しながら夕食を食べました。 業務のことからそれ以外のことまで幅広い話題で盛り上がり、よい交流の場になりました。

懇親会の様子(第1ターム)
懇親会の様子(第1ターム)

いただいた感想

今回のインターンでは、第1タームと第2タームそれぞれ4名の方にご参加いただきました。 参加者からいただいた感想を一部ご紹介いたします。

あっという間の4日間で「もう終わり?!」という感じです。それも4日間のインターンの内容がすごく濃くて楽しいものだったからだと思います。インターンではコンテンツ担当者やメンターの方々の説明やサポートがとてもわかりやすく、かみ砕いで説明してくれるのでスムーズに理解でき、とても勉強になりました。


業務の流れや、社内の雰囲気について知ることができてよかったです。私はこれが初めてのインターン参加で最初は不安がありましたが、演習や振り返りなど全てアプリ化されており、資料や説明もわかりやすく、インターンに集中することができました。


4日間という短い期間でしたが、非常に多くの学びがある有意義な時間でした。コンテンツに関しては、サイボウズでしか学べないような製品品質を向上する仕組みを深く知れるように組まれており、今後のキャリアを考える上で非常に参考になりました。 またPSIRTチーム内の交流などで雰囲気の良さがわかり、インターンに参加しないとわからないようなところまでしれて楽しかったです。

まとめ

去年に続き今年もオンラインでの開催となりましたが、2タームとも無事に終えることができました。 4日間と短い期間でしたが、会社の雰囲気やプロダクトセキュリティのことを知るよい機会になったのではないかなと思います。 インターンで用意されているコンテンツは、どれもCy-PSIRTの実際の業務に基づいた実践的な内容になっています。 ご参加いただいたインターン生の皆さんには、この経験や得た学びをこれからの活動に役立てていただけると嬉しいです。 今後の活躍を期待しております!

Cy-PSIRTでは一緒に働く仲間を募集中です。ご興味のある方は以下をご覧ください。

cybozu.co.jp

サイボウズサマーインターン2022 報告 〜 Kubernetes基盤開発コース&ストレージコース

こんにちは、Necoチームの鎌田阪上、そしてストレージチームの大神satです。

サイボウズは毎年サマーインターンシップを開催しています。2022年度のサマーインターンシップもさまざまなコースを用意しました。本記事では8月22日~9月2日の10日間にわたって開催された、Kubernetes基盤開発コース、およびストレージコースの模様をお届けいたします。

Kubernetes基盤開発コースではKubernetesを用いたサイボウズの新しいインフラ基盤であるNecoの開発業務を、ストレージコースではNecoの上に構築したストレージ基盤の開発業務を体験してもらいました。 今回のインターンシップでは、Kubernetes基盤開発コースには4名、ストレージコースには2名の学生さんが参加してくれました。

cybozu.co.jp

cybozu.co.jp

講義

Kubernetes基盤開発コース、およびストレージコースでは、数多くの講義や勉強会を実施しました。

  • Introduction to Kubernetes
    • Kubernetesの基礎についてハンズオン形式で学ぶ講義
  • Kubernetes勉強会
    • Kubernetesについてより深く学ぶ講義
  • サイボウズとOSS
    • 企業にとってのOSSの位置づけについての一般論、および、サイボウズについての具体例を紹介
  • つくって学ぶKubebuilder
    • Kubebuilderやcontroller-runtimeを利用したKubernetesコントローラの開発方法を学ぶチュートリアル
  • Ginkgo/GomegaによるKubernetes Operatorのテスト手法
    • GinkgoとGomegaというライブラリを利用したKubernetesコントローラのテスト手法
  • NecoWeekly
    • 最近気になるCloud Native関連のネタを共有する雑談会
  • その他、社内で開催されている勉強会

学生さんには各コースで必要となる知識に応じてこれらの講義に参加してもらい、基礎知識を身につけた上で以下のそれぞれのコースに分かれて実作業に入りました。

  • Kubernetes基盤開発コース
  • ストレージコース

ここからは2つのコースで具体的に取り組んだ内容について紹介します。

Kubernetes基盤開発コース

Kubernetes基盤開発コースでは、Kubernetesコントローラの開発を通じてKubernetesの基礎やコントローラの開発手法およびテスト手法を学んでもらうことを目的としています。 それぞれ2人ずつ、以下のチームに分かれて課題に取り組んでもらいました。

  • nyamberチーム
  • necotiatorチーム

nyamberチーム

Necoチームでは、これまでサーバーのプロビジョニングやKubernetesクラスタの構築、Kubernetes上で動作するアプリケーションの動作テストを自動化するために、仮想データセンタという仕組みを開発してきました。

さらに現在、Kubernetes上に仮想データセンタ環境を構築するためのKubernetesコントローラとしてnyamberを開発しています。 このチームでは、nyamberを題材にKubernetesコントローラの開発に取り組んでもらいました。

実施したタスク

nyamberチームでは、Kubernetesコントローラの開発を新しいカスタムリソースの定義から実装・テストまで一通り取り組みました。

  1. AutoVirtualDCカスタムリソースの仕様・定義
    nyamberには仮想データセンタの環境と1対1で対応するVirtualDCリソースがあります。 そのVirtualDCリソースをユーザ指定のスケジュールで自動作成・削除したり、 仮想データセンタ環境構築の失敗時に再作成する機能を持つのが今回作成したAutoVirtualDCリソースです。 そのような期待する動作を元に、どのようなフィールドやステータスを持てば良いかAutoVirtualDCのCustom Resource Definitionを設計しました。

    このタスクでkubebuilderの使い方などの開発ツールの利用を実践してもらいました。

  2. Reconcilerの実装
    AutoVirtualDCのフィールドにユーザが指定したスケジュールを元にVirtualDCリソースの作成・削除をする処理、 もし作られたVirtualDCリソースが仮想的なデータセンタの立ち上げに失敗していた場合に必要に応じて再作成する処理を実装しました。 時間に応じた期待する動作が存在するため時間を扱う部分をMockにしてテスト可能にする工夫をしました。

    Reconcilerの処理を冪等にするべきという前提がある状態で時間を扱うという内容の難しさから Reconcilerの実装方法やコードをテスト可能な状態に実装する方法を学んでもらえたのではないかと思います。

  3. Admission Webhookの実装
    Kubernetesにはリソースが保存される前にデフォルト値を設定したり、フィールドのvalidationを行うためのAdmission Webhookと呼ばれる機構が提供されています。 AutoVirtualDCリソースにも、想定しないフィールド指定の方法やフォーマットがあり、それらを保存前に確認するためのValidating Admission Webhookを実装しました。

    このタスクを通してKubernetesのリソースの保存時の動作についても理解してもらえたと思います。

  4. E2Eテストの実装
    最後にコントローラやWebhookの単体テストでまかなえない部分のテストをE2Eテストに追加しました。

これらのタスクでKubernetesコントローラの開発を一通り経験していただきました。さらにKubernetesやKubernetesコントローラの動作についても理解を深めていただけたと思います。

necotiatorチーム

サイボウズでは、複数のチームがKubernetesクラスタを共有するマルチテナンシー方式でクラスタを構築しています。

Kubernetesには、Namespaceごとにリソースの使用量を制限するResourceQuotaという仕組みがありますが、テナントごとにリソースの使用量を制限する仕組みはありません。 そこで、Kubernetes上でテナントごとのリソース使用量の上限を制限するコントローラとしてnecotiatorを開発することにしました。

このチームでは、社員と一緒にZoomでリモートモブプログラミングを行いながら、necotiatorの設計、実装、テストに携わってもらいました。

実施したタスク

  1. TenantResourceQuotaの定義
    サイボウズ社内でのKubernetesクラスタの使い方や現在の課題をインプットとしてnecotiatorの設計を行い、TenantResourceQuotaという新しいカスタムリソースを定義してもらいました。このカスタムリソースには、テナントチームが使用可能なリソースの総量と、テナントのNamespaceを指定するためのnamespace selectorを宣言するようにしました。

  2. コントローラとAdmission Webhookの実装
    necotiatorでは、テナント内のResourceQuotaの総量がTenantResourceQuotaの上限値を超えないよう制限する必要があります。そこでコントローラとしてResourceQuotaの総量を集計する機能を実装し、ResourceQuotaの総量が上限を超えないことを検証するAdmission Webhookを実装しました。

  3. 結合テストとE2Eテストの実装
    Ginkgo/GomegaというライブラリとEnvtestパッケージを使って、コントローラの各種機能を試験する結合テストと、Kubernetesクラスタ上にデプロイしたコントローラのE2E試験を実装してもらいました。学生さんと一緒に、必要なテストケースの列挙にも取り組みました。

このチームはKubernetesコントローラを新規開発するというタスクだったのですが、わずか2週間という短い期間の中で基本的な機能が動作するところまで完成させることができました。 この課題を通して、Kubernetesコントローラの動作や実装について深く理解できたのではないかと思います。 また、実装よりも多くのテストを書くという経験を通して、プロダクション環境で動かすシステムにおけるテストの重要性も知ることができたと思います。 さらには、KindTiltを活用した効率的な開発手法、Prometheus Collectorによるメトリクスの公開、Server-Side Applyによる差分適用手法など、発展的な内容にも取り組むことができました。 学ぶことの多い非常に濃密な2週間だったと思います。

ストレージコース

ストレージコースでは、2名のインターン生を受け入れ、実務に近い課題として以下を実施しました。

  • Upstream OSSへのPR投稿
  • CSI Driver E2Eモニタ開発

ストレージコースのインターンを主催しているCSAチームでも、普段の業務をモブプログラミングで行っています。インターンでも、インターン生2名と社員3名の合計5名でZoomを用いたリモートモブプログラミングを行いました。

Upstream OSSへのPR投稿

まず、最初の2日程で、インターン生お二人のそれぞれに1件ずつ、Upstream OSSとしてRookへのPR投稿を行っていただきました。

投稿していただいたPRはそれぞれ以下の通りです。

github.com github.com

この課題によって、OSS開発におけるコミュニティとのやり取りの仕方や、レビュー時のコード修正の方法などを学んでいただきました。

成果として、前者のPRはレビューの指摘事項は反映し終わり間もなくマージという所まで至ることができ、後者のPRはマージにまで至ることができました。

CSI Driver E2Eモニタ開発

CSAでは、Necoが提供するKubernetes環境上で動作するアプリケーションに向けて、Rook/CephやTopoLVMなどのCSI Driverによってストレージを提供しています。

ストレージの提供は、PVC(PersistentVolumeClaim)というリクエストに応じて自動的に行われるようにしています。ただし、これを監視するモニターがまだ無かったため、今回のインターンでインターン生の方々と共に開発を行いました。

ここでは、技術的な詳細には立ち入らず、どのような観点で何を行ったかを説明します。

対象の把握

まず、PVCに応じてストレージを提供できない状況はどのような状況なのか、そしてどのようなメトリクスでそれを判断できるのか、といった事を、実験用の環境で確認しました。実験により、問題の状況は様々なケースで発生し得る事が分かり、まずはその内の1つのケースに限定してモニターの開発を進めることにしました。(結果的にはこの1つのケースの実装で終わりました。)

ここでは、インターン生には「まずは対象をよく観察する」という事を学んでいただきました。

実装

設計についてはある程度事前に社員の側で用意していたため、それに沿った形でインターン生の方々に実装を行っていただきました。

成果としては、まだPoCの域を出ませんが、想定ケースで必要となるメトリクスを取得しVictoriaMetricsのメトリクスとしてエクスポートさせることができました。

実装は全てGo言語で行っていただき、ここでは、Go言語での実装ノウハウなどを学んでいただきました。

ストレージコースまとめ

この様に、ストレージコースのインターンでは、普段、CSAチームが業務として行っている課題の一部を切り出してインターン生の方々に行っていただきました。

CSAチームはOSSをフル活用したチームであり、今回のインターンを通してインターン生の方々から「インターン後もOSSへのコントリビュートを行ってみたい」というコメントが得られたことは良かったと思います。

まとめ

去年までもそうだったのですが、いずれのコースも始まったばかりのころは知らないことばかりで苦労しましたが、終盤に近付くにつれて講師が口を出さなくても学生たちが自律的にアイデアを出したり手を動かしたりできるようになる場面が増えました。頼もしい限りです。この経験が彼らの今後の人生を決める上で役に立つことを願っています。

最後になりますが、Necoチームやストレージチームでは一緒に働いてくれるかたを募集中です。以下募集要項を見て興味を持ったかたはぜひご応募ください。

cybozu.co.jp

cybozu.co.jp