<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:webfeeds="http://webfeeds.org/rss/1.0">
  <title>Infrastructure カテゴリ | フューチャー技術ブログ</title>
  <subtitle>Infrastructure カテゴリの記事一覧</subtitle>
  <icon>https://future-architect.github.io/feed_icon.png</icon>
  <logo>https://future-architect.github.io/apple-touch-icon.png</logo>
  <webfeeds:icon>https://future-architect.github.io/apple-touch-icon.png</webfeeds:icon>
  <webfeeds:accentColor>258fb8</webfeeds:accentColor>
  <link href="https://future-architect.github.io/categories/Infrastructure/atom.xml" rel="self"/>
  <link href="https://future-architect.github.io/categories/Infrastructure/"/>
  <updated>2026-06-16T15:00:00.000Z</updated>
  <id>https://future-architect.github.io/categories/Infrastructure/</id>
  <generator uri="https://hexo.io/">Hexo</generator>
  <entry>
    <title>非機能テストを記号接地する</title>
    <link href="https://future-architect.github.io/articles/20260617a/"/>
    <id>https://future-architect.github.io/articles/20260617a/</id>
    <published>2026-06-16T15:00:00.000Z</published>
    <updated>2026-06-16T15:00:00.000Z</updated>
    <author><name>清水利博</name></author>
    <content type="html"><![CDATA[<p>テスト連載2026 の3本目です。</p>
<h2 id="はじめに">はじめに</h2><p>TIG（Technology Innovation Group）の清水です。</p>
<p>あなた今回非機能テストやってみない？と言われた。けどそれってなんだっけ、という人向けの記事です。「非機能テスト」と聞いてなんとなくイメージが浮かぶようになる、が記事の目標です。</p>
<h2 id="そもそも-【非機能】-が分かりにくい">そもそも　【非機能】　が分かりにくい</h2><p>【非機能】という単語がそもそも分かりにくいです。</p>
<p>「キノウではナイもの」。なんだそれは。</p>
<p>絵だとこうです。うーむ分からん。</p>
<img fetchpriority="high" src="/images/2026/20260617a/IMG_0098.png" alt="IMG_0098.png" width="1200" height="800">

<p>非機能は【構造】と捉えると分かりやすいです。</p>
<p>機能（サービス）を支える構造、ですね。</p>
<p>絵はこうです。さっきより多少いいでしょうか。</p>
<p>でも具体、のイメージはまだつきにくいです。</p>
<img src="/images/2026/20260617a/IMG_0100.png" alt="IMG_0100.png" width="1200" height="800" loading="lazy">

<h2 id="門限が決まっている-→-性能要件">門限が決まっている　→ <strong>性能要件</strong></h2><p>映画館の【<strong>券売機</strong>】を例に考えます。</p>
<p>予定時刻で映画が始まってしまう。</p>
<p>開始5分前に50人来て購入・発券するなら、1分あたり10人を捌く必要がある。券売機が2台なら、1台はその半分捌ければ良いです。</p>
<p>業務 <strong>門限から考えて、どれくらいのスピードが必要か</strong> です。</p>
<p><strong>映画を楽しんでもらう・また来ようと思ってもらう</strong> を頂点として、券売機に求められる性能要件は何か、ですね。</p>
<img src="/images/2026/20260617a/IMG_0101.png" alt="IMG_0101.png" width="1200" height="800" loading="lazy">

<p>本当の映画館では、ポップコーンを買ったりトイレに行ったりの時間もあります。始まるといっても予告編があり、人の流量をどう見立てるかが重要です。家族なら人数分まとめて発券するでしょう。web予約側は20分前締め切り・劇場では開始直前まで、という条件もあります。映画館によっては上映開始後でも購入可能で、考え方の違いが門限に現れています。1人でぽっかり時間空いた・映画見るぞというとき、上映開始後に購入できると確かに嬉しいです。</p>
<p>券売機の要求を処理するシステム側から考えるとき、日本全国で100館・1館に5台券売機があるなら、500台の券売機が同時要求してきます。お客さんから券売機いくらなんでも遅すぎだろと思われると、次は違う映画館に行ってしまうかも知れません。それより前に現場が混乱します。あーまた、とニュースになるでしょう。目も当てられません。ということで機能と非機能を絵にします。</p>
<img src="/images/2026/20260617a/IMG_0102.png" alt="IMG_0102.png" width="1200" height="800" loading="lazy">

<p><strong>非機能</strong></p>
<p>ここではスピードを取り上げました。性能ですね。</p>
<p>空席確認から発券までに2時間かかったら、お客さんは違う映画館に行ってしまうでしょう。頑張って作った機能が全て台無しです。<strong>映画を楽しんでもらう・また来ようと思ってもらう</strong>というそもそもの話が、吹き飛んでしまいます。</p>
<p>非機能で転んで全てが台無しになるケースのイメージです。では何秒だったらOKなのか。処理量、同時性、画面フレームワーク特性、ネットワーク構成、排他制御、選択した各処理方式等含めて限界がある中で、経済合理性とバランスが取れ、構築期間を守ることができ、<strong>映画を楽しんでもらう</strong>を実現できるところが、目的地です。</p>
<h3 id="応答が遅いと怒られる">応答が遅いと怒られる</h3><p>「門限までに全部処理して！」以外に、画面レスポンスが遅くて怒られる、もあります。空席表示に10秒かかったら、まず「壊れてるな」と連打されます。普段みんなが使っているスマホアプリとの体感勝負になりやすいです。あまりに高性能を求めると、実現可能性に黄色信号が点るのと合わせて、費用にはねてきます。</p>
<h3 id="一度にお客がいっぱいやってくる">一度にお客がいっぱいやってくる</h3><p>人気アイドルグループの最終ツアー、となるとチケットサイトには一度に大勢のファンが押しかけます。想定を超えた要求がやってくると、システムはスローダウンし挙動が不安定になります。このような性質のシステムでは、性能破綻しないように流量制御機構を配備します。流量に合わせてシステムリソースを拡張する方式を採るケースもあります。「あなたは何番目」を見えるようにし、お客さんのイライラを鎮める工夫もするでしょう。あまりに大きな非機能（性能）要件から、新しい機能・アーキテクチャが必要になるケースです。<strong>全体が破綻しないように</strong>、システム挙動をいかに想定内・テスト範囲内に収めるか、が重要です。</p>
<p>全公演満席になるほどの人気グループであれば、チケットサイトが一時期ダウンしても、割当枚数は復旧後に販売できます。しかしサイト全体が重くなることで、他のコンサートや美術館チケットを買おうとしていたユーザーは競合サイトに流れるでしょう。流量制御機構を配備することで回避できます。</p>
<h2 id="テストする">テストする</h2><p>非機能要件定義で「この機能群を、<strong>こんなふうに支えてほしい</strong>」を確定します。その通りに動くかを、実際にやって証明するのが【非機能テスト】です。<br>ただ最後の最後にそもそもこれって。。。となると収拾がつかなくなります。このアーキテクチャで非機能要件が達成できるか、はアーキテクチャ決定時に確認する必要があります。これははじめての性能テスト にも記載されています。</p>
<p>サービスに必要な非機能 &#x3D; 機能を支える構造、は性能だけではありません。<br>代表的なものを普通のことばで表現するとこうです。</p>
<ul>
<li>どれくらいのスピードを出したいか？</li>
<li>どれくらい伸縮させたいか？</li>
<li>どれくらいの止まりにくさにしたいか？</li>
<li>5年間サービス維持できそうか？</li>
<li>どれくらいの耐攻撃性にしたいか？</li>
</ul>
<p>テストについて言えることがあります。</p>
<ul>
<li>「<strong>その条件で</strong> 動かしたなら大丈夫」</li>
<li>「動かしていないなら、いざという時に動かなくても不思議はない」</li>
</ul>
<p>そして非機能で転ぶと「特定機能の停止」にとどまらない全滅がありえます。熟慮し先を見ながら進めたいですね。</p>
<h2 id="おわりに">おわりに</h2><p>非機能テストってなんだっけ、という方がなんとなくイメージを持てるように、記事を書きました。なんとなくイメージついたかも。。？という方は是非一歩踏み出してください。</p>
<p>すでに、ガイドライン公開したよ記事 ＆ <strong>ガイドライン本体</strong>が公開されています！</p>
<ul>
<li>はじめての性能テストを公開しました</li>
<li>はじめての性能テスト</li>
</ul>
]]></content>
    <summary type="html">あなた今回非機能テストやってみない？と言われた。けどそれってなんだっけ、という人向けの記事です。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="テスト" scheme="https://future-architect.github.io/tags/%E3%83%86%E3%82%B9%E3%83%88/"/>
    <category term="非機能" scheme="https://future-architect.github.io/tags/%E9%9D%9E%E6%A9%9F%E8%83%BD/"/>
  </entry>
  <entry>
    <title>RFC 7807 から RFC 9457 で Web API のエラー表現で変わったこと</title>
    <link href="https://future-architect.github.io/articles/20260611a/"/>
    <id>https://future-architect.github.io/articles/20260611a/</id>
    <published>2026-06-10T15:00:00.000Z</published>
    <updated>2026-06-10T15:00:00.000Z</updated>
    <author><name>真野隼記</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2026/20260611a/top.jpg" alt="" width="600" height="335">

<h2 id="はじめに">はじめに</h2><p>TIG（Technology Innovation Group）の真野です。</p>
<p>HTTP API のエラーレスポンスを標準化する仕様、RFC 7807 “Problem Details for HTTP APIs” は 2016 年 3 月に公開され、以降 Web API のエラー表現のデファクトとして広く参照されてきました。その RFC 7807 ですが、約 7 年後の 2023 年 7 月に RFC 9457 に置き換えられていたのはご存知でしょうか。</p>
<p>RFC 9457 は後方互換で、差分は思ったより少ないのです。それでも「何が変わって、なぜ変わったのか」を押さえておくと、これから Web API のエラー設計をする際に判断しやすくなるでしょう。本記事では両 RFC の概要と差分、そして今後の設計指針を整理します。</p>
<h2 id="RFC-7807-Problem-Details-の標準化-2016年">RFC 7807: Problem Details の標準化 (2016年)</h2><p>RFC 7807 は 2016年3月に公開された Proposed Standard です。</p>
<p>API ごとにエラーの表現方法がバラバラで、クライアント側が毎回パース方法を変える必要があるという悩みを解決するために作られました。RFC 7807 は以下の 5 つのメンバと、<code>application/problem+json</code> というメディアタイプを定義しています。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>メンバ</th>
<th>役割</th>
</tr>
</thead>
<tbody><tr>
<td><code>type</code></td>
<td>問題の種類を識別する URI</td>
</tr>
<tr>
<td><code>title</code></td>
<td>人間向けの短い要約</td>
</tr>
<tr>
<td><code>status</code></td>
<td>HTTP ステータスコード</td>
</tr>
<tr>
<td><code>detail</code></td>
<td>問題の詳細説明</td>
</tr>
<tr>
<td><code>instance</code></td>
<td>特定の発生インスタンスを示す URI</td>
</tr>
</tbody></table></div>
<p>サンプルとして、2 歳になる我が子に「早くお風呂に入ろう」とリクエストしたところ、「今はおもちゃで遊びたい」と 400 エラーで返された状況を problem details で表現してみます。なお、RFC 原文のサンプルは「残高不足」を扱っています。</p>
<figure class="highlight http"><table><tr><td class="code"><pre><span class="line"><span class="meta">HTTP/1.1</span> <span class="number">400</span> Bad Request</span><br><span class="line"><span class="attribute">Content-Type</span><span class="punctuation">: </span>application/problem+json</span><br><span class="line"><span class="attribute">Content-Language</span><span class="punctuation">: </span>ja</span><br><span class="line"></span><br><span class="line"><span class="language-json"><span class="punctuation">&#123;</span></span></span><br><span class="line"><span class="language-json">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://example.com/probs/busy-with-toys&quot;</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;今はお風呂に入れません。&quot;</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">  <span class="attr">&quot;detail&quot;</span><span class="punctuation">:</span> <span class="string">&quot;今はおもちゃで遊びたい気分なので、もう少し待ってください。&quot;</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">  <span class="attr">&quot;instance&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/children/myson/bath-requests/20260420-0730&quot;</span><span class="punctuation">,</span></span></span><br><span class="line"><span class="language-json">  <span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="number">400</span></span></span><br><span class="line"><span class="language-json"><span class="punctuation">&#125;</span></span></span><br></pre></td></tr></table></figure>

<p>この「共通フォーマットで機械可読なエラーを返す」というアイデアは普及し、多くのフレームワーク・ライブラリが対応しました。</p>
<ul>
<li>RFC 7807 原文</li>
<li>日本語訳</li>
</ul>
<h2 id="RFC-9457-7807-を置き換える改訂版-2023年">RFC 9457: 7807 を置き換える改訂版 (2023年)</h2><p>RFC 9457 は 2023年7月に公開された Proposed Standard です。RFC 7807 を obsolete（廃止）する位置づけになっています。</p>
<p>Abstract にはこう書かれています。</p>
<blockquote>
<p>This document defines a “problem detail” to carry machine-readable details of errors in HTTP response content to avoid the need to define new error response formats for HTTP APIs. This document obsoletes RFC 7807.<br>(Google翻訳)<br>この文書は、HTTP API 用の新しいエラー応答フォーマットを定義する必要性を回避するために、HTTP レスポンスコンテンツにエラーの機械可読な詳細情報を含めるための「問題の詳細」を定義します。この文書は RFC 7807 を廃止します。</p>
</blockquote>
<p>「新しいフォーマットを定義するものではない」と明記されており、つまりフォーマットは 7807 と同じです。 <strong>完全な後方互換性が保たれている</strong> ため、7807 に準拠して実装したレスポンスはそのまま 9457 準拠として有効です。</p>
<ul>
<li>RFC 9457 原文</li>
<li>日本語訳</li>
</ul>
<h2 id="変更点">変更点</h2><p>RFC 9457 の Appendix D “Changes from RFC 7807” が挙げる変更点は3点です。</p>
<ol>
<li>Section 4.2 で、共通の問題型 URI を登録する IANA レジストリを新設した</li>
<li>Section 3 で、複数の問題をどう扱うべきかを明確化した</li>
<li>Section 3.1.1 で、デリファレンスできない <code>type</code> URI の使用に関するガイダンスを追加した</li>
</ol>
<p>加えて Section 5 の Security Considerations もやや強化されています。新しいフィールドの追加や、既存フィールドの削除は一切ありません。</p>
<h2 id="改善点①-type-URI-のガイダンス明確化">改善点①: <code>type</code> URI のガイダンス明確化</h2><h3 id="7807-時代の課題">7807 時代の課題</h3><p>RFC 7807 は「<code>type</code> URI はデリファレンス可能である必要はない」と書いていました。しかし実装者からすると、</p>
<ul>
<li>相対 URI を使ってよいのか？</li>
<li>存在しない URI を書いてよいのか？</li>
<li>人間向けのドキュメントはどこに置けばいいのか？</li>
</ul>
<p>といった具体的な判断材料が不足しており、迷いやすい状態でした。結果として、各社で運用がバラけたわけです。</p>
<h3 id="9457-での改善">9457 での改善</h3><p>RFC 9457 の Section 3.1.1 は以下の点を明確化しました。</p>
<ul>
<li><code>type</code> はあくまで識別子であり、HTTP で取得できなくてもよい</li>
<li>可能ならその URI に人間向けの説明（HTML）を置くことが望ましい</li>
<li>型固有の拡張メンバの仕様は、別途ドキュメント化すべき</li>
</ul>
<p>これで、<strong>社内向け API で相対 URI や URN を <code>type</code> に使う設計</strong>が、公式にお墨付きを得た形です。</p>
<h3 id="具体例">具体例</h3><p>子どもが夕食前に「ゼリーを食べたい」とリクエストしたものの、「夕食前のおやつはダメ」ルールで却下された、というケースを例に見てみます。</p>
<p><strong>❌ Before (7807 時代)</strong>: 絶対 URI でないと不安で、存在しない URL をわざわざ書く。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://api.example.com/problems/snack-before-dinner&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;夕食前のおやつは食べられません&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;detail&quot;</span><span class="punctuation">:</span> <span class="string">&quot;夕食まであと 30 分です。ゼリーは夕食の後に食べましょう。&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="number">422</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>この URL を開いても 404 なのですが、それでも「絶対 URI にしておけば安全かな」と書きがちでした。</p>
<p><strong>✅ After (9457)</strong>: 相対 URI や URN を堂々と使える</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/problems/snack-before-dinner&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;夕食前のおやつは食べられません&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;detail&quot;</span><span class="punctuation">:</span> <span class="string">&quot;夕食まであと 30 分です。ゼリーは夕食の後に食べましょう。&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="number">422</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>URN でも OK です。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;urn:example:problems:snack-before-dinner&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;夕食前のおやつは食べられません&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;detail&quot;</span><span class="punctuation">:</span> <span class="string">&quot;夕食まであと 30 分です。ゼリーは夕食の後に食べましょう。&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="number">422</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<h3 id="備考">備考</h3><p>「どんなドキュメントを、どこに、どの形式で置くか」まで踏み込んだ指針は RFC には書かれていません。そこは各組織の設計判断に委ねられています。</p>
<h2 id="改善点②-複数エラーの扱い">改善点②: 複数エラーの扱い</h2><h3 id="7807-時代の課題-1">7807 時代の課題</h3><p>フォームバリデーションのように「複数の問題を同時に返したい」場面に対する指針が、RFC 7807 にはありませんでした。結果、各フレームワーク・各社が独自拡張を持つことになりました。</p>
<ul>
<li>Spring の <code>invalid-params</code></li>
<li>JSON API 系の <code>errors</code></li>
<li>独自の <code>violations</code> &#x2F; <code>issues</code> など</li>
</ul>
<p>命名も構造もバラバラで、相互運用性が弱かったわけです。</p>
<h3 id="9457-での改善-1">9457 での改善</h3><p>RFC 9457 の Section 3 が、まず次の方針を示しました。</p>
<blockquote>
<p>When a problem in an HTTP response has multiple problems, it is RECOMMENDED that a single problem be chosen to represent the response, preferring the most relevant or urgent problem.<br>(Google翻訳)<br>HTTPレスポンスに複数の問題が含まれている場合、レスポンスを代表する問題として、最も関連性が高い、または緊急性の高い問題を優先して選択することが推奨されます。</p>
</blockquote>
<p>レスポンス全体としては単一の problem details を選ぶのが推奨、という方針です。そのうえで、関連する複数の問題を伝えたい場合は拡張メンバに含めればよいとし、Section 3.2 で <code>errors</code> 配列を拡張メンバの<strong>例</strong>として具体的に示しています。</p>
<p>朝の幼稚園準備で、検温忘れ・水筒が空・連絡帳未記入、名札がない靴下の利用という 4つのミスが一度に検出された、というケースを例にしてみます。</p>
<figure class="highlight http"><figcaption><span>✅️具体例</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta">HTTP/1.1</span> <span class="number">400</span> Bad Request</span><br><span class="line"><span class="attribute">Content-Type</span><span class="punctuation">: </span>application/problem+json</span><br><span class="line"><span class="attribute">Content-Language</span><span class="punctuation">: </span>ja</span><br><span class="line"></span><br><span class="line"><span class="language-prolog">&#123;</span></span><br><span class="line"><span class="language-prolog">  <span class="string">&quot;type&quot;</span>: <span class="string">&quot;/problems/kindergarten-preparation-failed&quot;</span>,</span></span><br><span class="line"><span class="language-prolog">  <span class="string">&quot;title&quot;</span>: <span class="string">&quot;幼稚園の持ち物に不備があります&quot;</span>,</span></span><br><span class="line"><span class="language-prolog">  <span class="string">&quot;errors&quot;</span>: [</span></span><br><span class="line"><span class="language-prolog">    &#123; <span class="string">&quot;detail&quot;</span>: <span class="string">&quot;朝の検温がまだ終わっていません&quot;</span>, <span class="string">&quot;pointer&quot;</span>: <span class="string">&quot;#/healthCheck/temperature&quot;</span> &#125;,</span></span><br><span class="line"><span class="language-prolog">    &#123; <span class="string">&quot;detail&quot;</span>: <span class="string">&quot;水筒が空のままです&quot;</span>, <span class="string">&quot;pointer&quot;</span>: <span class="string">&quot;#/items/waterBottle&quot;</span> &#125;,</span></span><br><span class="line"><span class="language-prolog">    &#123; <span class="string">&quot;detail&quot;</span>: <span class="string">&quot;連絡帳の家庭欄が未記入です&quot;</span>, <span class="string">&quot;pointer&quot;</span>: <span class="string">&quot;#/items/communicationNotebook&quot;</span> &#125;,</span></span><br><span class="line"><span class="language-prolog">    &#123; <span class="string">&quot;detail&quot;</span>: <span class="string">&quot;名札がついていない靴下を履いています&quot;</span>, <span class="string">&quot;pointer&quot;</span>: <span class="string">&quot;#/clothing/socks&quot;</span> &#125;</span></span><br><span class="line"><span class="language-prolog">  ]</span></span><br><span class="line"><span class="language-prolog">&#125;</span></span><br></pre></td></tr></table></figure>

<h3 id="備考-1">備考</h3><p>ただし、<code>errors</code> 配列は規範化されているわけではなく、あくまで <strong>例示</strong>です。位置指定の手段（JSON Pointer か JSONPath か独自キー名か）も自由で、依然として実装者の裁量は広く残っています。</p>
<h2 id="改善点③-共通型レジストリの新設">改善点③: 共通型レジストリの新設</h2><h3 id="7807-時代の課題-2">7807 時代の課題</h3><p>「Rate Limit 超過」「Quota 超過」のような、どの API でも共通で使えそうなエラー型でさえ、各社が独自の URI を定義していました。再利用と収束のための仕組みが仕様側になかったわけです。</p>
<h3 id="9457-での改善-2">9457 での改善</h3><p>RFC 9457 の Section 4.2 で IANA に HTTP Problem Types レジストリ が新設されました。登録テンプレートには以下の項目を含めます。</p>
<ul>
<li>Type URI</li>
<li>Title</li>
<li>Recommended HTTP status code</li>
<li>Reference</li>
</ul>
<h3 id="登録方法">登録方法</h3><p>登録ポリシーは <strong>Specification Required</strong> で、RFC 8126 Section 4.6 に基づきます。簡単に言うと以下です。</p>
<ul>
<li>登録したい問題型の仕様書（公開された安定したドキュメント）を用意する</li>
<li>上記テンプレートの項目を埋めて IANA に申請する</li>
<li><strong>Designated Expert</strong> がレビューして承認・却下を判断する</li>
<li>審査では「定義の明確性」「RFC 9457 の要件適合性」「コミュニティからのフィードバック」が考慮される</li>
</ul>
<p><strong>ベンダー固有・アプリケーション固有・デプロイ固有の値は登録不可</strong>とされており、広く再利用できる共通型だけが対象です。</p>
<p>プレフィックスとして <code>https://iana.org/assignments/http-problem-types#</code> を使うことも可能ですが、RFC 自身も「この URI は解決可能でない場合がある」と書いており、あくまで識別子として使う位置づけです。</p>
<h3 id="例示">例示</h3><p>子どもが 1 分間に 20 回「おかし食べたい」と連投してくる状況を想定します。API としては HTTP 共通の 429 Too Many Requests を返したいところですが、同じ 429 エラーでも実装ごとに <code>type</code> URI の付け方はバラバラでした。</p>
<p><strong>❌ Before (7807 時代)</strong>: 共通のエラーでも実装ごとに独自 URI を定義していた。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://a-family.example/problems/too-many-snack-requests&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Too Many Requests&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="number">429</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://b-family.example/problems/snack-request-cooldown&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Too Many Requests&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="number">429</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;urn:c-family:problems:snack-quota-exceeded&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Too Many Requests&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="number">429</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p><strong>✅ After (9457)</strong>: 共通型を IANA レジストリで共有できる仕組みが整った。</p>
<p>現状は <code>about:blank</code> のみ登録済みで、型に特別な意味を持たせない場合の標準値として使います。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;about:blank&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Too Many Requests&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="number">429</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;detail&quot;</span><span class="punctuation">:</span> <span class="string">&quot;おやつの要求が多すぎます。30 分後にまた声をかけてください。&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>将来的に Rate Limit 超過のような汎用エラー型が IANA に登録されれば、複数の API 間で <code>type</code> を共通化できるようになるというのがレジストリ新設の狙いになります。</p>
<h3 id="備考-2">備考</h3><p>2026年4月時点のレジストリに登録されているのは 3 件です。<code>about:blank</code> に加えて、<code>https://iana.org/assignments/http-problem-types#date</code> と <code>#ohttp-key</code> がありますが、後者 2 つはいずれも RFC 9458 Oblivious HTTP 由来で登録されたものです。汎用的な業務エラー型（Rate Limit 超過、バリデーションエラー等）が登録されているわけではありません。</p>
<h2 id="改善点④-Security-Considerations-の補強">改善点④: Security Considerations の補強</h2><h3 id="7807-時代の課題-3">7807 時代の課題</h3><p>RFC 7807 にもセキュリティ上の注意書きはありましたが、具体性に欠けていました。結果、<code>detail</code> フィールドにスタックトレースや内部パスがそのまま載る実装が散見される状態でした。</p>
<h3 id="9457-での改善-3">9457 での改善</h3><p>RFC 9457 の Section 5 はより踏み込んだ記述になっています。</p>
<ul>
<li>スタックダンプ・内部実装詳細の露出を避ける</li>
<li><code>instance</code> に不透明でない（デバッグ性のある）URL を置くと、情報漏洩経路になりうる</li>
<li>クライアントは信頼できないソースから来た problem details を、安易にそのまま表示しない（XSS 観点）</li>
</ul>
<h3 id="具体例-1">具体例</h3><p>冒頭のお風呂サンプルを引きずって、「子どもがお風呂に持ち込むおもちゃが電池式だったので業務ルールで弾いた」ケースをイメージしてみます。</p>
<p><strong>❌ Before (悪い例)</strong>: <code>detail</code> にスタックトレースをそのまま載せる。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/errors/bath-preparation-failed&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Bath preparation failed&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;detail&quot;</span><span class="punctuation">:</span> <span class="string">&quot;com.example.bathtime.exception.ToyNotWaterproofException: Battery-powered toy cannot be brought into the bathroom\n  at com.example.bathtime.validator.ToyWaterSafetyValidator.validate(ToyWaterSafetyValidator.java:58)\n  at com.example.bathtime.service.BathPreparationService.validateItems(BathPreparationService.java:34)\n  at com.example.bathtime.service.BathPreparationService.prepare(BathPreparationService.java:21)\n  at com.example.bathtime.controller.BathController.startBath(BathController.java:19)&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="number">422</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>パッケージ構成・例外クラス名・内部バリデータ構造まで透けて見えるため、攻撃者の下調べに使われかねません。</p>
<p><strong>✅ After (良い例)</strong>: 利用者向けは業務的な説明にとどめ、相関 ID で運用側から追跡可能にする。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/errors/toy-not-waterproof&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;このおもちゃはお風呂に持ち込めません&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;detail&quot;</span><span class="punctuation">:</span> <span class="string">&quot;電池で動くおもちゃはお風呂場に持ち込めません。別のおもちゃを選んでください。&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;instance&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/errors/trace/a1b2c3d4e5f6&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;status&quot;</span><span class="punctuation">:</span> <span class="number">422</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p><code>instance</code> には不透明な相関 ID（トレース ID 等）だけを入れ、サーバ側のログと突き合わせればデバッグできる運用にしておくのが定石です。</p>
<h3 id="備考-3">備考</h3><p>「どこまで出してよいか」の線引きは、公開 API（B2C）か社内 API かで大きく変わります。最終的には設計者が文脈を踏まえて判断するしかなく、RFC としては「気をつけろ」以上のことは書けないものと思います。</p>
<h2 id="フューチャーの-Web-API-設計ガイドラインでの扱い">フューチャーの Web API 設計ガイドラインでの扱い</h2><p>私がフューチャーの Web API 設計ガイドライン の執筆に関わった関係で、ガイドライン側との対応関係にも触れておきます。</p>
<p>ガイドラインではエラーレスポンスを RFC 9457 準拠で推奨しており、主な整合点は以下の通りです。</p>
<ul>
<li><code>type</code> は相対パス URI を推奨（9457 Section 3.1.1 の「デリファレンス不可でよい」ガイダンスに相当）</li>
<li>複数エラーは <code>errors</code> 配列で表現し、件数が 1 件でも配列形式で統一</li>
<li><code>errors[].detail</code> を必須、<code>pointer</code> を任意とする</li>
<li>エラー時の Content-Type は <code>application/problem+json</code> を推奨</li>
<li>B2C 公開 API では詳細情報を控え、内部 API では詳細を許容（9457 Section 5 Security Considerations と整合）</li>
</ul>
<p>興味ある方は、ぜひ ガイドライン本体 も読んでみてください。</p>
<h2 id="まとめ">まとめ</h2><p>RFC 7807 から RFC 9457 で変わったことは、フォーマットとしてはゼロに等しく、本質的には「<strong>実務で迷いやすかった部分のガイダンスが整理された</strong>」という位置づけのアップデートです。</p>
<p>新規の Web API 設計では、まず RFC 9457 を読み、<code>application/problem+json</code> を返すところから始めれば良いと思いました。差分が少ないこと自体が、この仕様がもう十分に枯れていることを示しているようで、個人的には読んでいて心地よい RFC の更新でした。</p>
]]></content>
    <summary type="html">HTTP API のエラーレスポンスを標準化する仕様、RFC 7807 Problem Details for HTTP APIs は 2016 年 3 月に公開され、以降 Web API のエラー表現のデファクトとして広く参照されてきました。その RFC 7807 ですが、約 7 年後の 2023 年 7 月に RFC 9457 に置き換えられていたのはご存知でしょうか。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="RFC" scheme="https://future-architect.github.io/tags/RFC/"/>
    <category term="Web" scheme="https://future-architect.github.io/tags/Web/"/>
    <category term="WebAPI" scheme="https://future-architect.github.io/tags/WebAPI/"/>
    <category term="エラーハンドリング" scheme="https://future-architect.github.io/tags/%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%8F%E3%83%B3%E3%83%89%E3%83%AA%E3%83%B3%E3%82%B0/"/>
  </entry>
  <entry>
    <title>はじめての性能テストを公開しました</title>
    <link href="https://future-architect.github.io/articles/20260610a/"/>
    <id>https://future-architect.github.io/articles/20260610a/</id>
    <published>2026-06-09T15:00:00.000Z</published>
    <updated>2026-06-09T15:00:00.000Z</updated>
    <author><name>武田大輝</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>TIG（Technology Innovation Group）の武田です。</p>
<p>はじめての性能テストという性能テストの入門ガイドラインを作成し、公開しました。<br>https://future-architect.github.io/arch-guidelines/documents/forPerformanceTest/performance_test.html</p>

<img fetchpriority="high" src="/images/2026/20260610a/image.png" alt="image.png" width="1200" height="570">


<p>本ガイドラインは、性能テストの計画から実行、分析に至るまでの実践的なプロセスを一通りまとめたものとなります。次のような方を主な読み手として想定しています。</p>
<ul>
<li>性能テストの経験がなく、何から手をつければよいかわからない方</li>
<li>これまでなんとなく性能テストを実施してきたが、考え方を体系的に整理したい方</li>
</ul>
<h2 id="ガイドライン作成のモチベーション">ガイドライン作成のモチベーション</h2><p>まずはじめに、性能テストは難しいです。私が難しいと考える理由は次の3点です。</p>
<ul>
<li><p><strong>技術的な視野の広さ</strong><br>フロントエンドから、バックエンドアプリケーション、ミドルウェア、データベース、そしてインフラまで、システムの全体を捉えて性能を見ていく必要があります。性能目標を達成できないときに、どこにボトルネックがあるのか「あたり」をつけ、クイックに原因特定から改善まで進めるには、特定の技術領域に偏らない幅広い知識と視野が欠かせません。</p>
</li>
<li><p><strong>業務ドメインの理解</strong><br>テストシナリオやデータパターンは、技術だけで決められるものではありません。対象システムがどのように使われ、いつアクセスが集中するのかといった、ビジネスコンテキストを理解したうえで初めて定義できるものです。</p>
</li>
<li><p><strong>多くの関係者の調整</strong><br>性能テストは巻き込むべき関係者が多くなります。アプリケーションやインフラの開発者はもちろん、目標値や完了基準を合意するビジネスサイドの方々、テスト環境やクラウドの利用申請に関わる方々など、多くの人を巻き込みながら調整を進めるコミュニケーションが求められます。</p>
</li>
</ul>
<p>こうした難しさゆえに、性能テストを計画段階からリードできる人は、どうしても一部のベテラン勢に限られてしまいがちです。経験則や暗黙知に支えられている部分も大きく、性能テストの経験が浅い若手やミドル層がリードするにはハードルが高いのが現状でした。</p>
<p>本ガイドラインは、こうした状況を少しでも変えたいという思いから作成しました。<br>性能テストの考え方や進め方を体系化することで、性能テスト未経験のエンジニアでも、計画からレポーティングまでを自信を持って推進できるようになることを目指しています。</p>
<h2 id="ガイドラインのポイント紹介">ガイドラインのポイント紹介</h2><p>全体としては、まず性能テストの前提となる知識（分類や性能指標などの考え方）を説明したうえで、<strong>計画 → 準備 → 実行 → チューニング → レポーティング</strong>という一連のステップに分けて論点や進め方を解説する構成になっています。</p>
<p>ここでは、その中でも特に重要なポイントをいくつか紹介します。</p>
<h3 id="性能テストの分類">性能テストの分類</h3><p>性能テストという言葉は解釈の揺れが大きく、負荷テストと同義に使われることもあれば、別物として区別されることもあります。本ガイドラインでは、性能に関するあらゆるテストを包括する概念として「性能テスト」を捉えたうえで、実施条件と目的に応じて5つに分類しています。</p>
<pre class="mermaid" data-mermaid="0917d0cc3c3f5ba613b44d5f6c15e53a276a3500337c5c2274637c05c5c95a54">---
config:
  theme: neutral
  layout: dagre
  look: handDrawn
  flowchart:
    htmlLabels: true
    nodeSpacing: 30
    rankSpacing: 50
---
flowchart TB
    classDef nodeLabel min-width:240px;

    Root["性能テスト"]

    subgraph Layer2 [" "]
      direction LR
      Data["データ量・サイズに焦点"]
      Load["アクセス数に焦点"]
      Time["稼働時間に焦点"]
    end

    subgraph Layer3 [" "]
      direction LR
      Normal["想定内（通常・ピーク）の負荷"]
      Over["想定以上の限界負荷"]
      Sudden["突発的・大規模な負荷"]
    end

    subgraph Layer4 [" "]
      direction LR
      Volume["ボリュームテスト"]
      Rush["ラッシュテスト"]
      Stress["ストレステスト"]
      Spike["スパイクテスト"]
      Long["ロングランテスト"]
    end

    Root --> Data
    Root --> Load
    Root --> Time

    Data --> Volume
    Load --> Normal & Over & Sudden
    Time --> Long

    Normal --> Rush
    Over --> Stress
    Sudden --> Spike

    style Layer2 fill:none,stroke:none
    style Layer3 fill:none,stroke:none
    style Layer4 fill:none,stroke:none</pre>

<h3 id="性能目標の定め方">性能目標の定め方</h3><p>性能テストの大前提として性能要件をどう定めるかという点が重要です。本ガイドラインでは「スループット」「処理時間」「リソース使用率」のそれぞれについて、目標値の決め方を手厚く扱いました。</p>
<p>例えば処理時間については、目標値を平均値や最大値ではなくパーセンタイル値（例. 95パーセンタイルで500ms以内）として定義する理由を説明しています。あわせて、既存システムをベースラインとするアプローチや、TTFBのような業界標準の指標、IPAの非機能要求グレードを参考にした目標値の設定例まで、現場でそのまま使える形に落とし込んでいます。</p>
<h3 id="性能テストの段取り">性能テストの段取り</h3><p>複数の種別をやみくもに実施するのではなく、ボリュームテスト → ラッシュテスト → ロングランテスト → ストレステストと観点を段階的に広げていく進め方を推奨しています。</p>
<p>テストの観点を「データ量」から「データ量 × 同時アクセス数」、そして「データ量 × 同時アクセス数 × 継続稼働時間」へと段階的に変化させていくことで、効率よくテストを積み上げられます。各テストには目的に対応した完了基準を定めており、「どうなったら次に進んでよいのか」がわかるようにしています。</p>
<img src="/images/2026/20260610a/image_2.png" alt="image.png" width="800" height="417" loading="lazy">

<h3 id="テストツールの選定">テストツールの選定</h3><p>負荷ツールやモニタリングツールの選び方も解説しています。負荷ツールについては、JMeter・k6・Gatling・Vegeta・AWS Distributed Load Testingといった代表的なツールを記述方法・実行効率・分散構成対応・習熟コストなどの観点で比較しました。</p>
<p>そのうえで、本ガイドラインでは <strong>k6</strong> を推奨しています。テストをJavaScriptのコードとして書けるためバージョン管理やCI&#x2F;CD連携と相性がよく、Go言語製で負荷生成の効率が高いことが主な理由です。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left"></th>
<th align="left">JMeter</th>
<th align="left">k6</th>
<th align="left">Gatling</th>
<th align="left">Vegeta</th>
<th align="left">AWS DLT</th>
</tr>
</thead>
<tbody><tr>
<td align="left">説明</td>
<td align="left">Java製OSS負荷テストツール - 多機能で利用実績も多い</td>
<td align="left">Go製OSS負荷テストツール - 省リソース、JSで記述しCI&#x2F;CD統合に優れる</td>
<td align="left">Scala製OSS負荷テストツール - コードで記述、HTMLレポートがリッチ</td>
<td align="left">Go製OSS CLI負荷テストツール- HTTP特化、シンプルで手軽だが機能は限定的</td>
<td align="left">AWSの分散負荷テストサービス - AWS上で大規模・サーバレス実行を自動化</td>
</tr>
<tr>
<td align="left">記述方法</td>
<td align="left">XML （GUIで生成）</td>
<td align="left">JavaScript</td>
<td align="left">Scala&#x2F;Java&#x2F;JavaScriptなど</td>
<td align="left">設定ファイル&#x2F;CLI引数</td>
<td align="left">JMeter&#x2F;k6&#x2F;Locustに対応</td>
</tr>
<tr>
<td align="left">GUI</td>
<td align="left">あり</td>
<td align="left">なし</td>
<td align="left">なし</td>
<td align="left">なし</td>
<td align="left">あり</td>
</tr>
<tr>
<td align="left">プロトコル</td>
<td align="left">✅️ HTTP&#x2F;SOAP&#x2F;JDBCなど多数（拡張可）</td>
<td align="left">⚠️ HTTP&#x2F;WebSocket&#x2F;gRPCなど（拡張可）</td>
<td align="left">⚠️HTTP&#x2F;WebSocket&#x2F;gRPCなど（拡張可）</td>
<td align="left">❌️ HTTP特化</td>
<td align="left">✅️ 多くのプロトコルに対応</td>
</tr>
<tr>
<td align="left">実行効率</td>
<td align="left">⚠️ 低（OSスレッドベース）</td>
<td align="left">✅️ 高</td>
<td align="left">✅️ 高</td>
<td align="left">✅️ 高</td>
<td align="left">✅️高</td>
</tr>
<tr>
<td align="left">シナリオ準備</td>
<td align="left">✅️ GUI記録機能あり、直感的</td>
<td align="left">✅️ コード記述、har-to-k6等あり</td>
<td align="left">✅️ コード記述、レコーダーあり</td>
<td align="left">✅️ 非常にシンプルな設定</td>
<td align="left">✅️ JMeter・k6・Locustに対応</td>
</tr>
<tr>
<td align="left">環境準備</td>
<td align="left">⚠️ Javaインストール・設定必要</td>
<td align="left">✅️ 単一バイナリ配置のみ</td>
<td align="left">⚠️ Java&#x2F;Scala環境設定必要</td>
<td align="left">✅️ 単一バイナリ配置のみ</td>
<td align="left">⚠️ 環境のデプロイが必要</td>
</tr>
<tr>
<td align="left">分散構成対応</td>
<td align="left">✅️ コントローラ&#x2F;ワーカー方式</td>
<td align="left">❌️ 手動での分散実行（k8s operatorあり）</td>
<td align="left">❌️ 手動での分散実行</td>
<td align="left">❌️ 手動での分散実行</td>
<td align="left">✅️AWS Fargate</td>
</tr>
<tr>
<td align="left">レポート</td>
<td align="left">✅️ GUI表示、HTMLレポート、CSV&#x2F;XML&#x2F;JTL出力</td>
<td align="left">✅️ 標準出力、JSON出力、CloudWatch連携</td>
<td align="left">✅️ HTMLレポート自動生成</td>
<td align="left">⚠️ 標準出力、CSV&#x2F;JSON出力</td>
<td align="left">✅️コンソール表示、CloudWatch連携</td>
</tr>
<tr>
<td align="left">商用サービス</td>
<td align="left">✅️ あり</td>
<td align="left">✅️ あり（k6 Cloud）</td>
<td align="left">✅️ あり（Gatling Enterprise）</td>
<td align="left">❌️ なし</td>
<td align="left">✅️</td>
</tr>
<tr>
<td align="left">習熟コスト</td>
<td align="left">⚠️ 独自GUIが複雑</td>
<td align="left">✅️ シンプルで容易</td>
<td align="left">⚠️ 独自DLSが複雑</td>
<td align="left">✅️ シンプルで容易</td>
<td align="left">⚠️ AWSインフラ知識が必要</td>
</tr>
<tr>
<td align="left">GitHubスター数</td>
<td align="left">9.1k</td>
<td align="left">29.4k</td>
<td align="left">6.8k</td>
<td align="left">24.7k</td>
<td align="left">-</td>
</tr>
</tbody></table></div>
<h3 id="チューニングポイント">チューニングポイント</h3><p>テスト実行中に特定したボトルネックをどう解消していくか、代表的なチューニング観点を「アプリケーションサーバ &#x2F; ランタイム」「アプリケーションロジック」「DBサーバ」「DBスキーマ &#x2F; DBクエリ」の各レイヤに分けて紹介しています。スケーリングやコネクションプール、SQLの実行計画やインデックスの最適化など、現場でよくあるポイントを一通り網羅しています。</p>
<h3 id="レポーティング">レポーティング</h3><p>最後に、テストの結果をどう評価・報告するかをまとめています。種別（ボリューム &#x2F; ラッシュ &#x2F; ロングラン &#x2F; ストレス）ごとに、性能指標・リソース使用状況・エラー内訳・最適なインフラ構成の検討結果といった、レポートに含めるべき観点を整理しました。性能テストは「測って終わり」ではなく、本番運用に耐えられること、そして残存リスクまでを説明できて初めて完了します。</p>
<p>このほかAPPENDIXとして、Core Web Vitalsを軸とした画面（フロントエンド）の性能テストや、計画時・実施後に使えるチェックリストも用意しています。</p>
<h2 id="おわりに">おわりに</h2><p>性能テストは「とりあえず負荷をかけて終わり」ではなく、目的を定め、合否を判断し、ボトルネックを潰しながら本番運用に耐えられることを示していく一連のプロセスです。本ガイドラインが、性能テストにこれから取り組む方や、自分たちの進め方を見直したい方にとっての一助となれば幸いです。</p>
<p>ガイドラインはGitHubの future-architect&#x2F;arch-guidelines で公開しており、Issueやプルリクエストでのフィードバックも歓迎しています。</p>
]]></content>
    <summary type="html">はじめての性能テストという性能テストの入門ガイドラインを作成し、公開しました。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="ガイドライン" scheme="https://future-architect.github.io/tags/%E3%82%AC%E3%82%A4%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3/"/>
    <category term="テスト" scheme="https://future-architect.github.io/tags/%E3%83%86%E3%82%B9%E3%83%88/"/>
    <category term="性能検証" scheme="https://future-architect.github.io/tags/%E6%80%A7%E8%83%BD%E6%A4%9C%E8%A8%BC/"/>
    <category term="非機能" scheme="https://future-architect.github.io/tags/%E9%9D%9E%E6%A9%9F%E8%83%BD/"/>
  </entry>
  <entry>
    <title>3年ぶりWindows環境構築のリトライと答え合わせ</title>
    <link href="https://future-architect.github.io/articles/20260603a/"/>
    <id>https://future-architect.github.io/articles/20260603a/</id>
    <published>2026-06-02T15:00:00.000Z</published>
    <updated>2026-06-02T15:00:00.000Z</updated>
    <author><name>棚井龍之介</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2026/20260603a/スクリーンショット_2026-05-12_14.25.06.png" alt="スクリーンショット_2026-05-12_14.25.06.png" width="600" height="585">

<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>本記事は、日本語 (JIS) 配列キーボードを使う Mac ユーザーが Windows 環境を組むケースを前提にしています。US 配列や、日本語入力を必要としない構成は対象外です。</p>
</div></div>

<h2 id="はじめに">はじめに</h2><p>こんにちは。棚井です。</p>
<p>2023 年、私は Mac から Windows への移行を機に、Windows 環境構築の記事を書きました。当時の私は非開発部門に所属しており、Mac で慣れ親しんだテキスト入力の操作感を Windows で再現することが、業務の死活問題でした。この記事は予想以上に多くの方に読んでいただき、私の試行錯誤がそのまま誰かの助けになったことを嬉しく思っています。</p>
<p>2026 年の今、私はエンジニアとして再び Windows 環境を構築しています。ただし今回は、Mac から Windows に移行するわけではありません。Mac の隣に、Windows を 1 台増やすためです。</p>
<p>業務で Claude Code を本格的に使い始めて思ったのは、現代の生産性とは「個人がハンドリングできる AI エージェントの並列数」とも言えるのではないか、ということです。マネジメント理論の「スパン・オブ・コントロール」(1 人の管理者が統制を保ったまま直接マネジメントできる部下の数は 5〜10 人が上限とされます) と同じ問題が、AI エージェントとの関わりにも起き始めています。Claude Code を 1 つ動かしているうちは出力をレビューする余裕があります。2 つ並列でも何とかなります。3 つになるとコンテキストの切り替えで頭が疲弊し始めます。 <strong>あなたは、何体の AI エージェントを同時に動かせますか?</strong> という問いが、現代のエンジニアに突きつけられているわけです。</p>
<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>この記事を見直している 2026 年 5 月 29 日、Opus 4.8 が使えるようになり、<code>/workflows</code> で複数のエージェントをまとめて並列に動かせるようになりました。こうなると、ボトルネックは「自分が何体まで面倒を見られるか」から、使える「予算」のほうに移ってきた気がします。<code>/effort</code> で ultracode を選ぶと大量のエージェントが一斉に走り出しますが、その分トークンの使用量も一気に跳ね上がるからです。スパン・オブ・コントロールの上限は、人間の側ではなく財布の側に引き直されつつあるのかもしれません。</p>
<p>https://www.anthropic.com/news/claude-opus-4-8</p>
</div></div>

<p>並列数をさらに増やすために、私は物理的に PC を増やすという選択をしました。1 台の Mac で全てのタスクをこなすよりも、Mac の中ではセッションを切り替えつつ、ドメインが大きく異なるタスクは別 PC (Windows) に振り分ける方が、頭の切り替えの負担と PC の負荷の両方を分散できると判断したからです。どうせ環境を増やすなら、普段使っている Mac ではなく、2023 年にセットアップした Windows を再構築するほうが面白そうです。そう思って、改めて Windows の環境構築に着手しました。この 3 年で出てきた新しいものも取り入れようと、関連ツールを片っ端から探しました。</p>
<h2 id="2026-年の-Windows-環境構築">2026 年の Windows 環境構築</h2><p>どのツールを使い、どう設定したかを順に書きます。順序は実際にインストールした順番に近い形にしてあります。読者の方が再現する際も、この順序で進めると手戻りが少ないと思います。</p>
<p>先に全体像を示します。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>設定対象</th>
<th>使用ツール</th>
</tr>
</thead>
<tbody><tr>
<td>物理キーのリマップ (Caps Lock → F13、Alt → Ctrl など)</td>
<td>SharpKeys</td>
</tr>
<tr>
<td>IME 切替 (無変換&#x2F;変換キーで IME ON&#x2F;OFF)</td>
<td>Microsoft IME</td>
</tr>
<tr>
<td>ランチャー (Ctrl + Space で起動)</td>
<td>PowerToys Command Palette</td>
</tr>
<tr>
<td>Mac 風のテキスト操作 (Caps Lock + H&#x2F;F&#x2F;B&#x2F;P&#x2F;N&#x2F;A&#x2F;E、zh&#x2F;zj&#x2F;zk&#x2F;zl で矢印)</td>
<td>AutoHotkey v2</td>
</tr>
<tr>
<td>クリップボード履歴 &#x2F; 拡張ペースト (Win + V、Win + Shift + V)</td>
<td>Windows 標準 + PowerToys Advanced Paste</td>
</tr>
<tr>
<td>ブラウザ (縦タブ + Workspaces)</td>
<td>Zen Browser</td>
</tr>
</tbody></table></div>
<h3 id="SharpKeys-のキーリマップ設定">SharpKeys のキーリマップ設定</h3><p>最初に物理キーボードのキー配置を決めます。Mac の指の動きを Windows で再現するための土台で、この後に入れるツールはこの配置を前提にしています。</p>
<p>SharpKeys は GitHub のリリースページから入手します。</p>
<p><code>.msi</code> または <code>.zip</code> をダウンロードします。業務 PC で管理者権限がない、あるいはレジストリを汚したくない場合は、ZIP 版のポータブル運用が便利です。</p>
<img src="/images/2026/20260603a/SharpKeys_のセットアップウィザード.png" alt="SharpKeys_のセットアップウィザード" width="390" height="318" loading="lazy">

<p>起動後、メイン画面の <strong>Add</strong> ボタンを押してリマップを追加します。</p>
<p>私が登録した 5 項目は以下の通りです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>From</th>
<th>To</th>
<th>スキャンコード対応</th>
</tr>
</thead>
<tbody><tr>
<td>Caps Lock</td>
<td>F13</td>
<td>00_3A → 00_64</td>
</tr>
<tr>
<td>半角&#x2F;全角</td>
<td>1 !</td>
<td>00_29 → 00_02</td>
</tr>
<tr>
<td>Left Alt</td>
<td>Left Ctrl</td>
<td>00_38 → 00_1D</td>
</tr>
<tr>
<td>Right Alt</td>
<td>Right Ctrl</td>
<td>E0_38 → E0_1D</td>
</tr>
<tr>
<td>ひらがな&#x2F;カタカナ</td>
<td>Right Ctrl</td>
<td>00_70 → E0_1D</td>
</tr>
</tbody></table></div>
<p>それぞれのリマップの意図を補足します。Caps Lock → F13 は、後で入れる AutoHotfkey のホットキーのトリガとして使うためです。Left Alt &#x2F; Right Alt → Ctrl は、Mac の Cmd の位置に Ctrl を持ってきて、ショートカットの指運びを Mac と揃えるための置き換えです。ひらがな&#x2F;カタカナ → Right Ctrl は、ホームポジション右側からも Ctrl を取れるようにする補強です。半角&#x2F;全角 → 1 ! は、IME 切替を変換&#x2F;無変換キーに集約した結果、Mac のキーボードには存在しなくて誤入力しやすい半角&#x2F;全角を「ただの 1 キー」に潰したかったからです。</p>
<p>5 項目を追加し終えたら、<strong>Write to Registry</strong> ボタンでレジストリに書き込みます。サインアウト → サインインで設定が反映されます。</p>
<img src="/images/2026/20260603a/SharpKeysメイン画面に5項目のリマップを設定した状態.png" alt="SharpKeysメイン画面に5項目のリマップを設定した状態" width="700" height="428" loading="lazy">

<p>Caps Lock を F13 にリマップした状態が、レジストリの Scancode Map レベルで有効になります。Windows のキーボードドライバが起動時に Scancode Map を読み込んで動くため、SharpKeys 自体は常駐しません。設定後は <code>SharpKeys.exe</code> を終了して構いません。</p>
<h3 id="IME-の設定">IME の設定</h3><p>タスクバー右下の「あ」または「A」を右クリック → 設定 → キーとタッチのカスタマイズを開き、「各キー&#x2F;キーの組み合わせに好みの機能を割り当てます」をオンにします。</p>
<p>無変換キーに IME オフ、変換キーに IME オンを割り当てます。Mac の英数キー&#x2F;かなキーと同じ操作感が Windows で再現できる、最重要の設定です。</p>
<img src="/images/2026/20260603a/MicrosoftIMEのキー割り当て画面.png" alt="MicrosoftIMEのキー割り当て画面" width="400" height="449" loading="lazy">

<h3 id="PowerToys-のインストール">PowerToys のインストール</h3><p>PowerToys は GitHub のリリースページから入手するのが確実です。</p>
<p><code>PowerToysUserSetup-x64.exe</code> を選びます。User Setup 版は管理者権限不要で、<code>%LOCALAPPDATA%\PowerToys</code> にインストールされます。業務 PC でも導入のハードルが低い形式です。</p>
<img src="/images/2026/20260603a/PowerToysのインストーラ.png" alt="PowerToysのインストーラ" width="500" height="338" loading="lazy">

<h3 id="PowerToys-Command-Palette-のホットキー設定">PowerToys Command Palette のホットキー設定</h3><p>PowerToys の左メニューから Command Palette を選択 → アクティブ化のショートカットを <code>Ctrl + Space</code> に変更します。</p>
<img src="/images/2026/20260603a/PowerToys_Command_Paletteのアクティブ化キーをCtrl+Spaceに設定.png" alt="PowerToys_Command_Paletteのアクティブ化キーをCtrl+Spaceに設定" width="800" height="182" loading="lazy">

<p>Mac で Raycast や Alfred を Cmd + Space で使っているなら、同じ感覚で起動できるようになります。</p>
<p>Ctrl + Space は Microsoft IME のデフォルトで IME ON&#x2F;OFF と衝突する可能性があります。先に IME の設定で無変換&#x2F;変換キーに IME 切替を割り当てておけば、Ctrl + Space を別用途に転用できます。</p>
<h3 id="AutoHotkey-v2-のインストールとスクリプト配置">AutoHotkey v2 のインストールとスクリプト配置</h3><p>AutoHotkey v2 を公式サイトから入手します。</p>
<img src="/images/2026/20260603a/AutoHotkey_v2のインストーラ.png" alt="AutoHotkey_v2のインストーラ" width="400" height="342" loading="lazy">

<p>公式インストーラを実行後、以下の内容で <code>rekeymap.ahk</code> を作成します。</p>
<figure class="highlight autohotkey"><table><tr><td class="code"><pre><span class="line"><span class="meta">#Requires AutoHotkey v2.0</span></span><br><span class="line"><span class="meta">#SingleInstance Force</span></span><br><span class="line"></span><br><span class="line"><span class="comment">; zh, zj, zk, zl で矢印文字</span></span><br><span class="line"><span class="title">:*:zh::</span>←</span><br><span class="line"><span class="title">:*:zj::</span>↓</span><br><span class="line"><span class="title">:*:zk::</span>↑</span><br><span class="line"><span class="title">:*:zl::</span>→</span><br><span class="line"></span><br><span class="line"><span class="comment">; F13 + キー で Mac 風のカーソル操作</span></span><br><span class="line"><span class="title">F13 &amp; H::</span>Send <span class="string">&quot;&#123;Blind&#125;&#123;Backspace&#125;&quot;</span></span><br><span class="line"><span class="title">F13 &amp; F::</span>Send <span class="string">&quot;&#123;Blind&#125;&#123;Right&#125;&quot;</span></span><br><span class="line"><span class="title">F13 &amp; B::</span>Send <span class="string">&quot;&#123;Blind&#125;&#123;Left&#125;&quot;</span></span><br><span class="line"><span class="title">F13 &amp; P::</span>Send <span class="string">&quot;&#123;Blind&#125;&#123;Up&#125;&quot;</span></span><br><span class="line"><span class="title">F13 &amp; N::</span>Send <span class="string">&quot;&#123;Blind&#125;&#123;Down&#125;&quot;</span></span><br><span class="line"><span class="title">F13 &amp; A::</span>Send <span class="string">&quot;&#123;Blind&#125;&#123;Home&#125;&quot;</span></span><br><span class="line"><span class="title">F13 &amp; E::</span>Send <span class="string">&quot;&#123;Blind&#125;&#123;End&#125;&quot;</span></span><br></pre></td></tr></table></figure>

<p>このファイルをダブルクリックで起動するとタスクトレイに緑の H アイコンが表示され、スクリプトが動作します。</p>
<p>Windows 起動時に自動実行させるには、<code>Win + R</code> で「ファイル名を指定して実行」を開き、<code>shell:startup</code> と入力してスタートアップフォルダを開きます。そこに <code>rekeymap.ahk</code> (または同ファイルへのショートカット) を配置すると、次回サインイン以降は自動でスクリプトが立ち上がるようになります。</p>
<p>このスクリプトの背景は、2023 年の記事 に書いた以下の引用がすべてです。</p>
<blockquote>
<p>Mac だと「Caps Lock + h, j, k, l」が、それぞれ「←、↓、↑、→」になりますし、「control + h, f, b, p, n, a, e」がそれぞれ「Backspace、→、←、↑、↓、Home、End」になります。Mac で身に染み込んだ指の動きを、Windows でも再現したい。これが私の出発点でした。</p>
</blockquote>
<p>Windows に持ち込んでいる操作系は 2 つあります。1 つは Google 日本語入力の挙動です。Google 日本語入力では、ローマ字入力中に <code>zh / zj / zk / zl</code> をタイプすると即座に矢印文字 <code>← / ↓ / ↑ / →</code> に変換されます。SNS やチャットで矢印を入れたいとき、Mac ではこの機能が当たり前のように動きます。Windows の Microsoft IME にはこの機能がないため、AutoHotkey のホットストリング機能で再現しています。<code>:*:zh::←</code> がその定義です。Google 日本語入力の変換とは違って IME の外でテキストを置き換える仕組みなので、日本語入力の途中ではなく直接入力のときに確実に効きます。</p>
<p>もう 1 つは Mac の Control キーによるカーソル操作です。Mac では <code>Control + H</code> で Backspace、<code>Control + F/B/P/N</code> で右&#x2F;左&#x2F;上&#x2F;下のカーソル移動、<code>Control + A/E</code> で行頭&#x2F;行末への移動ができます。Emacs キーバインドと呼ばれる体系で、テキスト入力をホームポジションから離れずに完結できます。Windows ではこれが標準で使えないため、AutoHotkey で <code>F13 &amp; H::Send &quot;&#123;Blind&#125;&#123;Backspace&#125;&quot;</code> のような形で再現しています。</p>
<p>F13 をトリガキーに選んでいる理由は、Caps Lock の押しっぱなし問題を回避するためです。Caps Lock はトグルキーなので、AutoHotkey で修飾キーとして使うと、大文字ロックの誤発火やキーが解放されない現象が起こりやすくなります。SharpKeys でレジストリレベルに Caps Lock → F13 を書き込んでおけば、トグル動作を持たない F13 を安全なトリガとして使えます。</p>
<p>物理キーボードのキー配置 (SharpKeys)、トリガキーへのリマップ (SharpKeys)、ホットキー定義 (AutoHotkey) の 3 つが揃って、ようやくホームポジションを崩さずに文字を入力できるようになります。試行錯誤の詳細は 2023 年の記事 に書いています。</p>
<h3 id="クリップボード履歴の有効化">クリップボード履歴の有効化</h3><p>Win + I → システム → クリップボード → クリップボードの履歴を ON にします。これで Win + V を押すと過去 25 件のコピー履歴がポップアップ表示されます。</p>
<img src="/images/2026/20260603a/Windows設定のクリップボードの履歴をONにした状態.png" alt="Windows設定のクリップボードの履歴をONにした状態" width="1200" height="242" loading="lazy">

<p>PowerToys Advanced Paste は別途 Win + Shift + V で起動します (筆者は起動キーを Ctrl + &#x2F; に変更しています)。リッチテキストをプレーンテキストに変換してペースト、HTML をマークダウンに変換、などの機能が使えます。AI 連携 (OpenAI &#x2F; Ollama &#x2F; Foundry Local 等) も設定可能ですが、業務 PC では一旦オフのままで実用上問題ありません。</p>
<img src="/images/2026/20260603a/PowerToys_Advan.png" alt="PowerToys_Advan" width="1200" height="436" loading="lazy">

<h3 id="Zen-Browser-のインストール">Zen Browser のインストール</h3><p>Mac で Arc を使い続ける場合の、Windows 側のメインブラウザとして Zen Browser を採用します。</p>
<img src="/images/2026/20260603a/ZenBrowser公式サイトのトップページ.png" alt="ZenBrowser公式サイトのトップページ" width="700" height="461" loading="lazy">

<p>ダウンロードした <code>.exe</code> を実行 → ウィザードに従ってインストール。SmartScreen 警告が出た場合は「詳細情報」→「実行」で進めます。初回起動時の Welcome 画面で、レイアウトを Single Toolbar に設定するのが Arc に最も近い操作感になります。</p>
<img src="/images/2026/20260603a/Zen_Browserのセットアップウィザード.png" alt="Zen_Browserのセットアップウィザード" width="300" height="226" loading="lazy">

<p>Compact Mode を有効化 (設定 → Look and Feel) し、Workspaces を作業内容ごとに作成します。</p>
<p>Chrome で日常的に使っている主要な拡張機能は、たいてい Firefox Add-ons にも同等品が公開されています。Zen Browser は Firefox ベースなので、これらをそのまま導入できます。</p>
<h3 id="設定後の構成">設定後の構成</h3><p>ここまでで、以下の構成が動いている状態になります。</p>
<ul>
<li>物理キーボード (MX KEYS mini) で Mac の指の動きが再現できる</li>
<li>Caps Lock + H&#x2F;F&#x2F;B&#x2F;P&#x2F;N&#x2F;A&#x2F;E で Mac 風のテキスト操作 (Backspace、カーソル移動、行頭&#x2F;行末)</li>
<li>zh&#x2F;zj&#x2F;zk&#x2F;zl で矢印文字入力</li>
<li>Ctrl + Space で PowerToys Command Palette</li>
<li>Win + V でクリップボード履歴</li>
<li>Win + Shift + V で拡張ペースト (Advanced Paste)</li>
<li>Zen Browser で Arc 風の作業空間</li>
</ul>
<p>ここまでの設定は、合計で 30 分程度で完了します。</p>
<h2 id="3-年で変わったもの、変わらなかったもの">3 年で変わったもの、変わらなかったもの</h2><p>ここまで紹介してきた設定を、2023 年の構成と比較してみます。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>カテゴリ</th>
<th>2023 年</th>
<th>2026 年</th>
<th>コンセプト</th>
</tr>
</thead>
<tbody><tr>
<td>キーボード</td>
<td>Logicool MX KEYS mini KX700GR</td>
<td>同じ</td>
<td>維持</td>
</tr>
<tr>
<td>IME</td>
<td>Microsoft IME (無変換&#x2F;変換キー設定)</td>
<td>同じ</td>
<td>維持</td>
</tr>
<tr>
<td>キーリマップ (低レイヤー)</td>
<td>Change Key</td>
<td>SharpKeys</td>
<td>維持 (Scancode Map 方式)</td>
</tr>
<tr>
<td>キーリマップ (高レイヤー)</td>
<td>AutoHotkey v2</td>
<td>AutoHotkey v2 (v1 は 2024&#x2F;3 EOL)</td>
<td>維持</td>
</tr>
<tr>
<td>ランチャー</td>
<td>ueli</td>
<td>PowerToys Command Palette</td>
<td>維持 (Microsoft が公式追従)</td>
</tr>
<tr>
<td>ファイル検索</td>
<td>Everything</td>
<td>PowerToys Command Palette の拡張機能</td>
<td>維持 (機能が公式統合)</td>
</tr>
<tr>
<td>クリップボード</td>
<td>Win + V (標準)</td>
<td>Win + V + PowerToys Advanced Paste</td>
<td>維持 (機能が公式統合)</td>
</tr>
<tr>
<td>ブラウザ</td>
<td>Chrome</td>
<td>Mac: Arc &#x2F; Windows: Zen Browser</td>
<td>Mac 環境構築で変化済み</td>
</tr>
</tbody></table></div>
<p>Windows の環境構築という範囲で見れば、3 年間で起きた変化はほとんどありません。物理キーボード、IME 設定、AutoHotkey のスクリプトは、2023 年から動かしているものがそのまま動いています。入れ替えたツールも、変わったのはツール側の事情だけです。Change Key は 2012 年 4 月の v1.50 で更新が止まったので、保守が続いていて ARM64 版もある SharpKeys に乗り換えました。ランチャーやファイル検索、クリップボード履歴は、サードパーティの機能が PowerToys や OS 標準に取り込まれたので、公式版を使っているだけです。どれも提供形態が変わっただけで、私のやりたいことは何も変わっていません。</p>
<p>ブラウザだけは 2023 年と 2026 年で違うように見えますが、これは Windows の環境構築とは別の話です。2025 年 2 月に公開した Mac 環境構築の記事 で書いた Chrome から Arc&#x2F;Zen Browser への乗り換えを、Windows にも反映させた結果です。Arc は 2025 年 5 月 26 日にメンテナンスモードに入り、Windows 版は未完成のまま終わるので、Windows 側は Zen Browser を選んでいます。</p>
<p>ブラウザでも、同じことが起き始めています。2025 年に私は Arc の縦タブ、Workspaces、Compact Mode、Split View に衝撃を受けましたが、1 年後の 2026 年には Chrome でも縦タブが正式機能になりました。挑戦的なツールが先に面白い機能を作り、メジャーなツールが後追いで取り込んでいく。ueli から PowerToys Command Palette、Everything から PowerToys のファイル検索、これと同じ動きです。</p>
<p>個別のツールに執着し続けるのは、3 年スパンで見れば現実的ではありません。それよりも、自分が何のためにそのツールを使っているのかを書いておくほうが、長く役に立ちます。私の場合、それは「Mac の指の動きを Windows で再現したい」だけです。Control + H で Backspace を打てる、Control + F&#x2F;B&#x2F;P&#x2F;N でカーソルを動かせる、Control + A&#x2F;E で行頭・行末に飛べる、Google 日本語入力で zh&#x2F;zj&#x2F;zk&#x2F;zl と打って矢印を入力できる。これらを Mac と同じ感覚で Windows でも使えれば、それで十分です。</p>
<p>ここは 3 年では陳腐化しませんでした。Windows の API も Mac の API も大きくは変わっていませんし、人間の指の動きはもっと変わりません。だからツールが何度入れ替わっても、指の動きを再現するという目的さえ書き留めておけば、毎回そこから組み直せます。これは 3 年経ってからようやく実感できたことです。</p>
<h2 id="おわりに">おわりに</h2><p>業務で Claude Code を使うために増設した Windows 機は、3 年前にセットアップした Windows と、ほとんど同じ構成で動き始めました。並列で動かせる AI エージェントの数を増やしたい、というのが今回の動機でした。Mac の隣に Windows を置くというハードウェアの判断は新しかったものの、その Windows をどう設定するかについては、すでに 2023 年の私が答えを出していました。新しいツールを探したのに、3 年前の私の発想を超えるものは何も見つかりませんでした。</p>
<p>PC のキーボードに両手で入力するスタイルが変わらない限り、ホームポジションを崩さない設定へのこだわりは変わりません。ということは、3 年後、また Windows をセットアップする機会が来たら、私はまた同じことを書くのかもしれません。もしくは、そもそも「両手でキーボードに入力する」という前提を覆すような、とんでもないデバイスの登場が待ち遠しいです。</p>
]]></content>
    <summary type="html">日本語 配列キーボードを使う Mac ユーザーが Windows 環境を組むケースを前提にしています。US 配列や、日本語入力を必要としない構成は対象外です。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="Mac" scheme="https://future-architect.github.io/tags/Mac/"/>
    <category term="Windows" scheme="https://future-architect.github.io/tags/Windows/"/>
    <category term="環境構築" scheme="https://future-architect.github.io/tags/%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89/"/>
  </entry>
  <entry>
    <title>分散システム入門: 信頼性の低いネットワークを再現してみる</title>
    <link href="https://future-architect.github.io/articles/20260427a/"/>
    <id>https://future-architect.github.io/articles/20260427a/</id>
    <published>2026-04-26T15:00:00.000Z</published>
    <updated>2026-04-26T15:00:00.000Z</updated>
    <author><name>内堀航輝</name></author>
    <content type="html"><![CDATA[<p>春の入門祭り2026の4本目です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは、自宅サーバの運用って実質盆栽だよなって思い始めてる、盆栽未経験の内堀です。今回は自宅サーバでネットワークフォルトを再現してみたよというお話です。</p>
<p>分散システムを勉強していると「ノードが完全に死ぬよりも、中途半端に死んでいるほうが厄介」という話に必ず出会います。完全に死ねばクラスタが検知して切り離せますが、半分生きているとシステムからは正常に見えてしまい、悪さをし続けるからです。というわけで、分散システム入門の第一歩として、今回はこの「中途半端に死んだノード」を自宅サーバ上で再現してみます。具体的には、50%パケロスする設定をノードに仕込み（本記事ではゾンビノードと呼びます）、クラスタへの影響を測定します。</p>
<p>結論から書くと、3ノードのKubernetesクラスタで1ノードのネットワークだけ半壊させたところ、クラスタ全体のスループットが1&#x2F;14、p99レイテンシが15倍に悪化しました。以下、この数字に至るまでの話を書いていきます。</p>
<h2 id="実験の概要">実験の概要</h2><h3 id="構成">構成</h3><p>3ノード（home-lab-1, home-lab-2, home-lab-3）のk3sクラスタで、以下を用意します。</p>
<ul>
<li>nginxのDeployment（replicas&#x3D;3、各ノードに1Podずつ配置されるようtopologySpreadConstraintsで制約）</li>
<li>上記を束ねるClusterIP Service</li>
<li>計測用のデバッグPod（curlとohaが入ったalpineベース、control-planeで元々負荷の軽いhome-lab-1に固定）</li>
</ul>
<h3 id="実験の流れ">実験の流れ</h3><p>以下の3段階で計測し、それぞれを比較します。</p>
<ol>
<li>ベースライン計測：パケットロスなしの状態で計測</li>
<li>半壊状態の計測：worker2（home-lab-3）のOS上で <code>tc qdisc add ... netem loss 50%</code> を実行し、パケットロス50%を発生させた状態で計測</li>
<li>完全停止状態の計測：worker2上で <code>systemctl stop k3s-agent</code> を実行し、Kubernetesから完全に切り離された状態で計測</li>
</ol>
<p>各段階で、デバッグPodからService経由でnginxを叩いて挙動を観察します。具体的には、軽い疎通確認としてcurlを60回ループで回して各Podへの振り分けを見て、その後ohaで20000リクエスト投げてSuccess rate、RPS、レイテンシ分布を計測します。</p>
<h2 id="実験">実験</h2><h3 id="実験1：ベースラインの計測">実験1：ベースラインの計測</h3><p>まずはパケットロスなしの状態で計測します。比較対象になる数字を取るのが目的です。</p>
<p>Podが3ノードに1つずつ分散配置されていること、ServiceのEndpointsに3つのPod IPが揃っていることを確認します。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl -n zombie-exp get pod -o wide</span></span><br><span class="line">NAME                     READY   STATUS    RESTARTS   AGE   IP            NODE</span><br><span class="line">nginx-84888755c4-l7xtn   1/1     Running   0          13h   10.42.2.82    home-lab-3</span><br><span class="line">nginx-84888755c4-vf9zs   1/1     Running   0          20h   10.42.1.213   home-lab-2</span><br><span class="line">nginx-84888755c4-z94f4   1/1     Running   0          20h   10.42.0.31    home-lab-1</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl -n zombie-exp get endpoints nginx</span></span><br><span class="line">NAME    ENDPOINTS                                    AGE</span><br><span class="line">nginx   10.42.0.31:80,10.42.1.213:80,10.42.2.82:80   20h</span><br></pre></td></tr></table></figure>

<p>3Pod、3ノードに分散、Endpointsに全部入っている状態。期待通りです。</p>
<h4 id="疎通確認">疎通確認</h4><p>デバッグPodからcurlを60回ループで回して、各Podにほぼ均等に振り分けられていることを見ます。nginxはpostStartで自分のhostname（&#x3D;Pod名）をindex.htmlに書き込んでいるので、レスポンスを見ればどのPodが応答したかわかります。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- sh -c <span class="string">&#x27;for i in $(seq 1 60); do curl -s --max-time 2 http://nginx.zombie-exp.svc.cluster.local/ || echo FAIL; done | sort | uniq -c&#x27;</span></span></span><br><span class="line">    19 nginx-84888755c4-l7xtn</span><br><span class="line">    19 nginx-84888755c4-vf9zs</span><br><span class="line">    22 nginx-84888755c4-z94f4</span><br></pre></td></tr></table></figure>

<p>19&#x2F;19&#x2F;22でほぼ均等。FAILは0件。</p>
<h4 id="定量計測">定量計測</h4><p>ohaで20000リクエスト、コネクション並列度30、<code>--disable-keepalive</code> でリクエストごとにTCP接続を張り直す設定で計測します。keep-aliveを切っているのは、後段の半壊状態でTCPハンドシェイクのSYNパケットがロスする様子をはっきり見るためです（keep-aliveを有効にすると同じコネクションを使い回してしまい、ロスの影響が見えにくくなります）。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- oha -n 20000 -c 30 -t 2s --disable-keepalive http://nginx.zombie-exp.svc.cluster.local/</span></span><br><span class="line"></span><br><span class="line">Summary:</span><br><span class="line">  Success rate: 100.00%</span><br><span class="line">  Requests/sec: 1620.8811</span><br><span class="line"></span><br><span class="line">Response time distribution:</span><br><span class="line">  50.00% in 2.0985 ms</span><br><span class="line">  90.00% in 69.1044 ms</span><br><span class="line">  99.00% in 108.9357 ms</span><br><span class="line"></span><br><span class="line">Status code distribution:</span><br><span class="line">  [200] 20000 responses</span><br></pre></td></tr></table></figure>

<p>20000リクエスト全成功、1620RPS、p50が2ms、p99が109ms。自宅サーバの3ノードk3sクラスタとしてはこんなもんかなと思います。<br>これがベースラインの数字。以降、半壊と完全停止の結果はこれと比較していきます。</p>
<p>ちなみに、実行中は以下の画像のように、レイテンシ分布が更新されながら進んでいくのが見えます。</p>
<img fetchpriority="high" src="/images/2026/20260427a/image.png" alt="image.png" width="1200" height="661">

<h3 id="実験2：home-lab-3をゾンビノードにして計測">実験2：home-lab-3をゾンビノードにして計測</h3><p>worker2（home-lab-3）にパケットロス50%を注入します。Linuxの <code>tc</code> コマンドの <code>netem</code> モジュールを使います。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">ssh worker2 <span class="string">&quot;sudo tc qdisc add dev eth0 root netem loss 50%&quot;</span></span></span><br></pre></td></tr></table></figure>

<p>これでeth0から出入りするパケットの50%がランダムに落ちるようになります。</p>
<h4 id="Kubernetesから見たノードの状態">Kubernetesから見たノードの状態</h4><p>ここが今回の実験の核心です。50%のパケロスを発生させた直後の状態を見ます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl get nodes</span></span><br><span class="line">NAME         STATUS   ROLES           AGE   VERSION</span><br><span class="line">home-lab-1   Ready    control-plane   73d   v1.34.3+k3s3</span><br><span class="line">home-lab-2   Ready    &lt;none&gt;          73d   v1.34.3+k3s3</span><br><span class="line">home-lab-3   Ready    &lt;none&gt;          73d   v1.34.3+k3s3</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl -n zombie-exp get endpoints nginx</span></span><br><span class="line">NAME    ENDPOINTS                                    AGE</span><br><span class="line">nginx   10.42.0.31:80,10.42.1.213:80,10.42.2.82:80   20h</span><br></pre></td></tr></table></figure>

<p>home-lab-3は<strong>Readyのまま</strong>。Endpointsからも外れていません。これがゾンビノードです。</p>
<p>なぜこうなるかというと、Kubernetesはkubeletがapiserverに対して定期的にハートビートを送ることでノードの生死を判定しています。50%のパケロスがあっても、TCPの再送機構によってハートビートはなんとか届くので、Kubernetesから見ればノードは正常稼働中ということになります。</p>
<h4 id="疎通確認-1">疎通確認</h4><p>次に、curlでの各Podへの振り分け状況を確認します。FAILが7件発生。home-lab-3上のPod（l7xtn）に振り分けられたリクエストは、半分くらいは2秒のタイムアウト内に応答が返ってきますが、半分くらいはパケロスによって失敗してFAIL扱いになります。</p>
<p>ベースラインではFAIL&#x3D;0だったところに、いきなり12%が失敗するようになりました。「ノードはReady、でもユーザのリクエストは落ちる」が起きている状態です。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- sh -c <span class="string">&#x27;for i in $(seq 1 60); do curl -s --max-time 2 http://nginx.zombie-exp.svc.cluster.local/ || echo FAIL; done | sort | uniq -c&#x27;</span></span></span><br><span class="line">      7 FAIL</span><br><span class="line">     18 nginx-84888755c4-l7xtn</span><br><span class="line">     18 nginx-84888755c4-vf9zs</span><br><span class="line">     17 nginx-84888755c4-z94f4</span><br></pre></td></tr></table></figure>

<h4 id="定量計測-1">定量計測</h4><figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- oha -n 20000 -c 30 -t 2s --disable-keepalive http://nginx.zombie-exp.svc.cluster.local/</span></span><br><span class="line"></span><br><span class="line">Summary:</span><br><span class="line">  Success rate: 90.50%</span><br><span class="line">  Total:        219.8244 sec</span><br><span class="line">  Requests/sec: 90.9817</span><br><span class="line"></span><br><span class="line">Response time distribution:</span><br><span class="line">  50.00% in 0.0008 sec</span><br><span class="line">  90.00% in 0.8245 sec</span><br><span class="line">  99.00% in 1.6341 sec</span><br><span class="line"></span><br><span class="line">Error distribution:</span><br><span class="line">  [1899] timeout</span><br></pre></td></tr></table></figure>

<p>数字を並べてみるとこんな感じ。</p>
<ul>
<li>Success rateが90.5%に低下</li>
<li>1899件のタイムアウトエラー</li>
<li>RPSが1620→91と、ベースラインの5.6%まで激減</li>
<li>20000リクエスト消化に12秒だったのが、220秒</li>
<li>p99が109ms→1634msと、約15倍に悪化</li>
</ul>
<p>正直、ここまで悪化するとは思っていませんでした。1台のゾンビノードが混ざったことで、クラスタ全体のスループットが18分の1まで落ち込みました。<br>面白いのがp50で、0.8msとベースラインの2.1msより速く見えます。これは「運よくロスに当たらず1発で通ったリクエスト」が半分弱あって、それらは普通に高速だからです。ロスに当たったリクエストはTCPの再送タイムアウト(初期RTOが1秒)に引っかかってp90以降で秒オーダーまでぶっ飛びます。「半分は普通に速い、半分は秒オーダーで遅い」という2山型(バイモーダル)の分布になっています。</p>
<h3 id="実験3：home-lab-3を完全停止させて計測">実験3：home-lab-3を完全停止させて計測</h3><p>最後に、worker2のk3s-agentを完全に止めて計測します。「ネットワーク半壊」と「ノード完全停止」のどちらがマシか、という比較が目的です。</p>
<h4 id="Kubernetesから見たノードの状態-1">Kubernetesから見たノードの状態</h4><p>実験2ではhome-lab-3はReadyでしたが、今回はNotReadyになりました。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl get nodes</span></span><br><span class="line">NAME         STATUS     ROLES           AGE   VERSION</span><br><span class="line">home-lab-1   Ready      control-plane   73d   v1.34.3+k3s3</span><br><span class="line">home-lab-2   Ready      &lt;none&gt;          73d   v1.34.3+k3s3</span><br><span class="line">home-lab-3   NotReady   &lt;none&gt;          73d   v1.34.3+k3s3</span><br></pre></td></tr></table></figure>

<p>PodとEndpointsの状態も確認します。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl -n zombie-exp get pod -o wide</span></span><br><span class="line">NAME                     READY   STATUS        RESTARTS   AGE   IP            NODE</span><br><span class="line">nginx-84888755c4-blc65   0/1     Pending       0          27s   &lt;none&gt;        &lt;none&gt;</span><br><span class="line">nginx-84888755c4-l7xtn   1/1     Terminating   1          14h   10.42.2.82    home-lab-3</span><br><span class="line">nginx-84888755c4-vf9zs   1/1     Running       0          21h   10.42.1.213   home-lab-2</span><br><span class="line">nginx-84888755c4-z94f4   1/1     Running       0          21h   10.42.0.31    home-lab-1</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl -n zombie-exp get endpoints nginx</span></span><br><span class="line">NAME    ENDPOINTS                      AGE</span><br><span class="line">nginx   10.42.0.31:80,10.42.1.213:80   21h</span><br></pre></td></tr></table></figure>

<p>home-lab-3上のPod（l7xtn）がTerminatingになり、EndpointsからもIP 10.42.2.82 が外れました。Service経由のトラフィックは健全な2Podだけに流れます。新しいPod（blc65）はPendingですが、各ノードに1Podまでの制約をかけているので置き場所がない、というだけで、Endpointsには影響しません。期待通りの挙動です。</p>
<h4 id="疎通確認-2">疎通確認</h4><p>curlで各Podへの振り分け状況を確認します。<br>FAIL&#x3D;0。home-lab-3上のPod（l7xtn）はEndpointsから外れているので、振り分け先には現れません。健全な2Podだけで応答が返っています。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- sh -c <span class="string">&#x27;for i in $(seq 1 60); do curl -s --max-time 2 http://nginx.zombie-exp.svc.cluster.local/ || echo FAIL; done | sort | uniq -c&#x27;</span></span></span><br><span class="line">     34 nginx-84888755c4-vf9zs</span><br><span class="line">     26 nginx-84888755c4-z94f4</span><br></pre></td></tr></table></figure>

<h4 id="定量計測-2">定量計測</h4><figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> kubectl -n zombie-exp <span class="built_in">exec</span> debug -- oha -n 20000 -c 30 -t 2s --disable-keepalive http://nginx.zombie-exp.svc.cluster.local/</span></span><br><span class="line"></span><br><span class="line">Summary:</span><br><span class="line">  Success rate: 100.00%</span><br><span class="line">  Requests/sec: 1267.3896</span><br><span class="line"></span><br><span class="line">Response time distribution:</span><br><span class="line">  50.00% in 1.9522 ms</span><br><span class="line">  90.00% in 91.6390 ms</span><br><span class="line">  99.00% in 193.9529 ms</span><br><span class="line"></span><br><span class="line">Status code distribution:</span><br><span class="line">  [200] 20000 responses</span><br></pre></td></tr></table></figure>

<p>Success rateは100%に戻りました。RPSは1267で、ベースラインの78%程度。Pod数が3→2に減ったので、スループットは下がりましたが、それだけです。p99も194msとベースラインの109msから少し悪化していますが、実験2の1634msと比べれば誤差みたいなものです。<br>Kubernetesがhome-lab-3をNotReadyと判定し、Endpointsから自動で外してくれたおかげで、リクエストはちゃんと返ってきます。ゾンビノードが混ざっているときと比べると、随分と平和な数字です。</p>
<h2 id="実験結果のまとめ">実験結果のまとめ</h2><p>3つの実験結果を表にまとめます。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>指標</th>
<th>ベースライン</th>
<th>ゾンビノードあり</th>
<th>完全停止</th>
</tr>
</thead>
<tbody><tr>
<td>Success rate</td>
<td>100.00%</td>
<td>90.50%</td>
<td>100.00%</td>
</tr>
<tr>
<td>RPS</td>
<td>1620</td>
<td>91</td>
<td>1267</td>
</tr>
<tr>
<td>p50</td>
<td>2.1 ms</td>
<td>0.8 ms</td>
<td>1.9 ms</td>
</tr>
<tr>
<td>p90</td>
<td>69 ms</td>
<td>824 ms</td>
<td>92 ms</td>
</tr>
<tr>
<td>p99</td>
<td>109 ms</td>
<td>1634 ms</td>
<td>194 ms</td>
</tr>
<tr>
<td>timeout</td>
<td>0</td>
<td>1899</td>
<td>0</td>
</tr>
<tr>
<td>20000リクエスト消化時間</td>
<td>12 sec</td>
<td>220 sec</td>
<td>16 sec</td>
</tr>
</tbody></table></div>
<p>ゾンビノードありと完全停止を並べてみると、こうです。</p>
<ul>
<li>スループット:完全停止の1&#x2F;14</li>
<li>p99:完全停止の8.4倍</li>
<li>Success rate:完全停止より9.5ポイント低下</li>
</ul>
<p>ノードが「壊れている」という点はどちらも同じです。違うのは、Kubernetesがそれを検知できるかどうか。検知できれば勝手に退避してくれるけど、検知できなければ放置されたまま。たったそれだけの差で、結果がガラッと変わりました。</p>
<p>冒頭で書いた「中途半端に死んでるほうが厄介」を、実験を通して確認できました。</p>
<h2 id="おわりに">おわりに</h2><p>今回はシンプルなHTTPリクエストのみを確認しましたが、これがDB接続のような状態を持つ処理であれば、コネクションを掴んだまま離さないリクエストがプールを食いつぶし、連鎖的な障害を招くことは容易に想像できます。</p>
<p>しかも厄介なことに、Kubernetes側の仕組みだけでは半壊状態を検知することが難しく、ノードがReadyである限り異常として処理されません。実際にネットワークフォルトを再現してみたことで、ゾンビノードが全体にどのような被害を及ぼすのか、解像度が上がりました。</p>
<p>それにしても、不調なのに「大丈夫です」と返すWorkerと、それを真に受けて普通に仕事を振り続けるControl plane。この関係はどこか見覚えがあって、なんだか親近感が湧くのは僕だけでしょうか。</p>
]]></content>
    <summary type="html">自宅サーバでネットワークフォルトを再現してみたよというお話です。分散システムを勉強していると「ノードが完全に死ぬよりも、中途半端に死んでいるほうが厄介」という話に必ず出会います。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="Kubernetes" scheme="https://future-architect.github.io/tags/Kubernetes/"/>
    <category term="k3s" scheme="https://future-architect.github.io/tags/k3s/"/>
    <category term="ネットワーク" scheme="https://future-architect.github.io/tags/Network/"/>
  </entry>
  <entry>
    <title>自宅だと apt update ができなかった話（WSL2 + 社内VPN環境での名前解決の遅延）</title>
    <link href="https://future-architect.github.io/articles/20260421b/"/>
    <id>https://future-architect.github.io/articles/20260421b/</id>
    <published>2026-04-20T15:00:01.000Z</published>
    <updated>2026-04-20T15:00:01.000Z</updated>
    <author><name>清水雄一郎</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2026/20260421b/top.avif" alt="" width="1200" height="634">

<h2 id="1-はじめに">1. はじめに</h2><p>こんにちは。HealthCare Innovation Group (HIG)の清水雄一郎です。<br>本記事は、春の入門祭り2026の1日目の記事です。</p>
<p>新年度・新チーム配属のこの季節、「開発環境のセットアップで丸一日溶かした」という経験をされている方も多いのではないでしょうか。特にエンタープライズ企業の「プロキシ・VPN配下」での開発環境構築は、ドキュメント通りに進めても謎のエラーに阻まれがちです。<br>プロキシに関しては、過去の記事<sup id="fnref:5">1</sup><sup id="fnref:6">2</sup><sup id="fnref:7">3</sup>もぜひご確認ください。</p>
<p>少し前（2026&#x2F;01頃）に私が遭遇した事象は、WSL2環境で「自宅に帰ると<code>apt update</code>できない」というものです。<br>当時を振り返ると、次のような状況でした。</p>
<ul>
<li>オフィスに出社してネットワークに繋ぐと、<code>apt update</code> が成功する</li>
<li>自宅（無線&#x2F;有線LAN&amp;VPN接続中）だと、 <code>apt update</code> が<strong>失敗する</strong></li>
<li>ただし、<strong>iPhoneのテザリング＋VPN</strong> の場合、<code>apt update</code> が<strong>成功する</strong></li>
</ul>
<p>結論から言うと、今回のケースではDNSの順序を変えることで解決しました。<br>その頃のSlackスレッドやGeminiとのチャット履歴を遡りつつ、体験記兼トラブルシューティングログとして残します。</p>
<h2 id="2-調査編-泥沼のトラブルシューティング">2. 調査編: 泥沼のトラブルシューティング</h2><h3 id="2-0-前提">2.0. 前提</h3><p>本記事は、次の環境下で確認したものです。<br>※Windows側のバージョンは、事象発生時から少し変わっているかもしれません。</p>
<details><summary>環境情報</summary>

<figure class="highlight text"><table><tr><td class="code"><pre><span class="line"># Windows</span><br><span class="line">エディション Windows 11 Pro</span><br><span class="line">バージョン 24H2</span><br><span class="line">OS ビルド 26100.8037</span><br><span class="line"></span><br><span class="line"># WSL2（Ubuntu）</span><br><span class="line">PRETTY_NAME=&quot;Ubuntu 22.04.5 LTS&quot;</span><br><span class="line">NAME=&quot;Ubuntu&quot;</span><br><span class="line">VERSION_ID=&quot;22.04&quot;</span><br><span class="line">VERSION=&quot;22.04.5 LTS (Jammy Jellyfish)&quot;</span><br><span class="line">VERSION_CODENAME=jammy</span><br><span class="line">ID=ubuntu</span><br><span class="line">ID_LIKE=debian</span><br><span class="line">HOME_URL=&quot;https://www.ubuntu.com/&quot;</span><br><span class="line">SUPPORT_URL=&quot;https://help.ubuntu.com/&quot;</span><br><span class="line">BUG_REPORT_URL=&quot;https://bugs.launchpad.net/ubuntu/&quot;</span><br><span class="line">PRIVACY_POLICY_URL=&quot;https://www.ubuntu.com/legal/terms-and-policies/privacy-policy&quot;</span><br><span class="line">UBUNTU_CODENAME=jammy</span><br></pre></td></tr></table></figure>

</details>

<h3 id="2-1-エラーログ">2.1. エラーログ</h3><p>まずは事象を整理するため、発生していたエラーログを確認します。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> apt update</span></span><br><span class="line">...</span><br><span class="line">エラー:5 http://archive.ubuntu.com/ubuntu jammy-updates InRelease</span><br><span class="line">  サーバからの読み込みに失敗しました - read (104: 接続が相手からリセットされました) [IP: 10.x.x.x 8080]</span><br><span class="line">エラー:6 http://archive.ubuntu.com/ubuntu jammy-backports InRelease</span><br><span class="line">  10.x.x.x:8080 (10.x.x.x) へ接続できませんでした。接続がタイムアウトしました [IP: 10.x.x.x 8080]</span><br><span class="line">エラー:3 http://security.ubuntu.com/ubuntu jammy-security InRelease</span><br><span class="line">  サーバからの読み込みに失敗しました - read (104: 接続が相手からリセットされました) [IP: 10.x.x.x 8080]</span><br><span class="line">...</span><br><span class="line">W: いくつかのインデックスファイルのダウンロードに失敗しました。 これらは無視されるか、古いものが代わりに使われます。</span><br></pre></td></tr></table></figure>

<p><code>10.x.x.x:8080</code>は、社内プロキシサーバーです。<br>「タイムアウト」と「接続が相手からリセットされました」が混在しており、この時点では原因が全く特定できていません。</p>
<p>試しに同じ社内プロキシを経由している <code>wget</code> &#x2F; <code>curl</code> コマンドを試しましたが、成功することを確認できました。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">wget google.com</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">→ 200 OK で正常に取得できる</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -I -L google.com</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">→ 200 OK</span></span><br></pre></td></tr></table></figure>

<p>※いま振り返ると、http://archive.ubuntu.com/ubuntu宛に<code>wget</code> &#x2F; <code>curl</code> コマンドを試した方が良かったかもしれません。</p>
<p>ここで一人では抱え込めないと判断し、Slackでチームメンバーに助けを求め、Geminiにも相談しながら仮説検証を始めました。<br>他の人に相談することは、私の環境だけの問題かどうか切り分けるという1つの検証と言えると思います。</p>
<img src="/images/2026/20260421b/Slackでチームに相談している様子.png" alt="Slackでチームに相談している様子" width="593" height="134" loading="lazy">

<h3 id="2-2-仮説①-プロキシ設定が間違っている？">2.2. 仮説① プロキシ設定が間違っている？</h3><p>最初に疑ったのは、やはりプロキシ周りです。<code>apt</code>は<code>wget</code>&#x2F;<code>curl</code>とは別経路の設定を見るため、<code>apt</code>自体のプロキシ設定を確認します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cat</span> /etc/apt/apt.conf</span><br><span class="line">Acquire::http::Proxy  <span class="string">&quot;http://username:password@proxy.example.com:8080&quot;</span>;</span><br><span class="line">Acquire::https::Proxy <span class="string">&quot;http://username:password@proxy.example.com:8080&quot;</span>;</span><br></pre></td></tr></table></figure>

<p>設定済みでした。環境変数も<code>apt</code>と同じ設定値が入っています。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">env</span> | grep -i proxy</span><br><span class="line">...</span><br><span class="line">http_proxy=http://username:password@proxy.example.com:8080</span><br><span class="line">https_proxy=http://username:password@proxy.example.com:8080</span><br><span class="line">...</span><br></pre></td></tr></table></figure>

<p><code>apt</code>のプロキシ設定は、問題なさそうです。<br>またこの時、環境変数を引き継ぐため、<code>sudo -E apt update</code>を実行してみましたが、最初と同様のエラーになりました。</p>
<h3 id="2-3-仮説②～⑤-プロキシ設定以外はどう？">2.3. 仮説②～⑤ プロキシ設定以外はどう？</h3><p>プロキシ設定以外に考えられる原因をGeminiやメンバーに聞いて片っ端から試しました。結論から言うと、いずれも空振りでした。</p>
<h4 id="仮説②-社内プロキシの証明書が未インストール？">仮説② 社内プロキシの証明書が未インストール？</h4><p>証明書周りはプロキシの次にトラブルが多い印象なので、試しに手動で入れ替えて確認します。<br>→ 変化なし</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 非推奨。次のコードブロックの手順を推奨。</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">rm</span> /etc/ssl/certs/ca-certificates.crt</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">cp</span> path/to/ca.crt /etc/ssl/certs/ca-certificates.crt</span><br><span class="line"><span class="built_in">sudo</span> apt update  <span class="comment"># 変化なし</span></span><br></pre></td></tr></table></figure>

<p>原因特定のために<code>/etc/ssl/certs/</code>配下の証明書を直接操作しましたが、本来は<code>update-ca-certificates</code>を使う方が正しいです。<br>推奨される方法は、次の通りです。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">cp</span> path/to/ca.crt /usr/local/share/ca-certificates/corp-proxy.crt</span><br><span class="line"><span class="built_in">sudo</span> update-ca-certificates</span><br><span class="line"><span class="built_in">sudo</span> apt update</span><br></pre></td></tr></table></figure>

<h4 id="仮説③-WSLのHyper-Vファイアウォールが遮断している？">仮説③ WSLのHyper-Vファイアウォールが遮断している？</h4><p>「デフォルトでオンになっているHyper-V FirewallをOFFにすると<code>apt</code>が通ることがある」という情報を教えてもらい、「WSL Settings」のGUIから無効化してみました。<br>→ 変化なし</p>
<img src="/images/2026/20260421b/WSL_Settings_で_Hyper-V_Firewall_を無効化した画面.png" alt="WSL_Settings_で_Hyper-V_Firewall_を無効化した画面" width="1200" height="645" loading="lazy">

<h4 id="仮説④-MTUの設定が経路と合っていない？">仮説④ MTUの設定が経路と合っていない？</h4><p>Geminiにエラーログを渡して相談すると、MTU（Maximum Transmission Unit）<sup id="fnref:1">4</sup>の影響を疑うよう提案を受けました。VPN環境下では実効MTUが小さくなり、デフォルト1500バイトのままだと通信に失敗するケースがあるとのこと。<br>→ こちらも変化なし</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> ip <span class="built_in">link</span> <span class="built_in">set</span> dev eth0 mtu 1300</span><br><span class="line"><span class="built_in">sudo</span> apt update  <span class="comment"># 変化なし</span></span><br></pre></td></tr></table></figure>

<p>後から振り返ると、MTU調整は「大きいパケットだけ落ちる」症状に有効な対策で、数十KBのリクエストで失敗する今回の症状とは整合しません。生成AIの提案を鵜呑みにして試した回り道でした。</p>
<h4 id="仮説⑤-archive-ubuntu-comが遠いから？">仮説⑤ <code>archive.ubuntu.com</code>が遠いから？</h4><p>リポジトリを日本のミラーサーバーに変更してみます。<br>→ ログは割愛しますが、タイムアウトエラーになってしまいます。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> sed -i.bak -E <span class="string">&#x27;s!http://(archive|security)\.ubuntu\.com!http://jp.archive.ubuntu.com!g&#x27;</span> /etc/apt/sources.list</span><br><span class="line"><span class="built_in">sudo</span> apt update  <span class="comment"># タイムアウトエラー</span></span><br></pre></td></tr></table></figure>

<h3 id="2-4-仮説⑥-接続するネットワークの問題？">2.4. 仮説⑥ 接続するネットワークの問題？</h3><p>出社する日が多く、自宅でのみ発生する問題の解消を後回しにしていたのですが、オフィスにいる時に時間ができたので再度向き合ってみることにしました。<br>VPN接続時に発生するため、オフィスにいながらBYODであるiPhoneのテザリングに切り替えて、VPN接続してみます。<br>なんと、<code>apt update</code>が<strong>正常終了する</strong>ことに気が付きます。整理すると次の状況です。</p>
<ul>
<li>オフィス無線LAN → OK</li>
<li>自宅（無線&#x2F;有線）LAN＋VPN → NG</li>
<li>iPhoneテザリング＋VPN → OK 🤔</li>
</ul>
<p>ひとまず成功したことに喜びつつ、ずっとテザリングで業務するわけにはいきません。<br>VPNがすべてダメというわけではないと分かったので、他の手立てを考えてみました。</p>
<h2 id="3-原因編-名前解決を疑う">3. 原因編: 名前解決を疑う</h2><p><code>wget</code>は通って<code>apt</code>は通らない、かつ、同じVPNを使っていても回線（自宅LAN&#x2F;テザリング）によって成否が変わることが分かりました。<br>つまり原因は「どの回線でも共通に使われる設定」ではなく、<strong>「回線ごとに切り替わる何か」</strong> にあると考えました。</p>
<p>回線ごとに切り替わるものの1つに、ホストであるWindowsが参照するDNSがあります。<br>WSL2は、デフォルトでWindows側のDNSを継承するため、回線が変わればWSLが使うDNSも変わります。</p>
<p>ここから、<strong>名前解決（DNS）の速度</strong>を疑うことにしました。<br>※名前解決とは、ドメイン名（例: <code>archive.ubuntu.com</code>）をIPアドレスに変換する処理のことです。</p>
<p><code>apt update</code>は、内部で複数のリポジトリに問い合わせているため、1問い合わせあたりの名前解決が遅いとそれが積み重なり、接続タイムアウトに抵触しやすいのではないかと考えました（最初に試した<code>wget</code> &#x2F; <code>curl</code> は単発リクエストなので、1回の遅延くらいは待てる、という仮説）。</p>
<p>実際に、名前解決の時間を計測してみます。<code>getent hosts</code>は、指定したホストの情報を取得するコマンドです。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="keyword">time</span> getent hosts proxy.example.com</span><br><span class="line">10.x.x.x    proxy.example.com</span><br><span class="line"></span><br><span class="line">real    0m15.225s</span><br><span class="line">user    0m0.001s</span><br><span class="line">sys     0m0.004s</span><br></pre></td></tr></table></figure>

<p>それが<strong>たった1件の社内ホスト情報取得に15秒</strong>かかっていました。</p>
<p>ちなみに、その時の<code>/etc/resolv.conf</code>の中身はこうでした。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">cat</span> /etc/resolv.conf</span><br><span class="line">nameserver 172.x.x.x    <span class="comment"># Windowsホスト側の仮想ルーター</span></span><br><span class="line">nameserver 10.x.x.x     <span class="comment"># 開発環境構築時に追記したもの</span></span><br><span class="line">search example.com      <span class="comment"># 開発環境構築時に追記したもの</span></span><br></pre></td></tr></table></figure>

<p><code>172.x.x.x</code>は、WSL2がデフォルトで参照するWindowsホスト側の仮想ルーターです。<br>Windows側の設定を引き継いで名前解決してくれる便利な仕組みですが、私の環境かつVPN接続状態ではボトルネックになっていると考えました。</p>
<h2 id="4-解決編-etc-resolv-confへの記載順序を変える">4. 解決編: <code>/etc/resolv.conf</code>への記載順序を変える</h2><p>原因が掴めれば対処は単純です。WSLのデフォルト仮想DNS（172.x.x.x）より先に、社内DNSに直接問い合わせるよう、<code>/etc/resolv.conf</code>の<code>nameserver</code>順序を並び替えます。</p>
<p>Linuxのリゾルバは、<code>/etc/resolv.conf</code>の<code>nameserver</code>を<strong>上から順に</strong>試すようです。<sup id="fnref:2">5</sup><sup id="fnref:3">6</sup>先頭から応答が返ればそこで終了、タイムアウトすれば次へフォールバックする仕組みです。1問い合わせあたりデフォルト5秒 × 複数回リトライすることで<sup id="fnref:4">7</sup>、先頭が遅いと十数秒単位で待たされていたと推測します。<br>つまり、社内DNSを先頭に置くだけで、ほぼ全ての問い合わせが1行目で即解決するようになるはずです。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> vim /etc/resolv.conf</span><br></pre></td></tr></table></figure>

<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"># /etc/resolv.conf</span><br><span class="line">nameserver 10.x.x.x   # 一番上に変更</span><br><span class="line">nameserver 172.x.x.x</span><br><span class="line">search example.com</span><br></pre></td></tr></table></figure>

<p>効果を計測します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="keyword">time</span> getent hosts proxy.example.com</span><br><span class="line">10.x.x.x    proxy.example.com</span><br><span class="line"></span><br><span class="line">real    0m0.033s</span><br><span class="line">user    0m0.003s</span><br><span class="line">sys     0m0.007s</span><br></pre></td></tr></table></figure>

<p><strong>15.225秒 → 0.033秒。</strong>約<strong>460倍</strong>の改善です。<br>この状態で<code>apt update</code>を叩くと、自宅（無線&#x2F;有線）LAN＋VPN環境で、一発で成功することを確認できました🎉</p>
<p>ここで忘れてはいけないのですが、<code>/etc/resolv.conf</code>は、WSLを再起動するとデフォルトで自動再生成され、上の変更が上書きされて消えてしまいます。<br>「せっかく直したのに翌朝また詰まった」を防ぐため、<code>/etc/wsl.conf</code>で自動生成を無効化しておきます。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> vim /etc/wsl.conf</span><br></pre></td></tr></table></figure>

<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"># /etc/wsl.conf</span><br><span class="line">[network]</span><br><span class="line">generateResolvConf = false</span><br></pre></td></tr></table></figure>

<h2 id="5-おわりに">5. おわりに</h2><p>今回の泥沼を振り返ると、最も反省すべき点は、<strong>「十分な評価をしないまま、ありがちな原因を手当たり次第試し続けた時間」</strong> の長さでした。</p>
<p>生成AIにエラーログを投げると、どんなに長くても中身を読んで自分が思いつかない次の一手を考えてくれます。<br>特に、環境構築周りは色々な層に原因が潜んでいるため、これまで自分よりも生成AIの方が得意な分野だと思っていました。<br>もちろん多くの場合その通りだと思いますが、今回のケースのようにうまくいかない時もあります。<br>改めて、開発環境構築において「何ができて何ができなかったか」を整理することはもちろん、<strong>「何が『どこまで』できているか」を「計測する」</strong> ことが重要だと実感しました。</p>
<p>これから開発環境を構築する皆さんは、詰まった際に生成AIに「何をやるといいか」と同時に、「何を計測すると原因が測れるか」を聞いてみると解決の近道になるかもしれません。<br>そしてぜひ、詰まった経緯と解決策を記事にしてネットに公開してください！<br>今度は、生成AIが学習して解決がもっと早まるかもしれません……！</p>
<h2 id="参考">参考</h2><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:5"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">TCP Proxyを作って面倒なProxy設定を一掃する ～Rust製moproxyとnftablesによる透過プロキシ設定～</span> ↩</li><li id="fn:6"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">ローカルプロキシで認証プロキシの煩わしさを解消！</span> ↩</li><li id="fn:7"><span style="vertical-align: top; padding-right: 10px;">3.</span><span style="vertical-align: top;">ProxyとDockerと新人社員と時々わたし</span> ↩</li><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">4.</span><span style="vertical-align: top;">https://wa3.i-3-i.info/word13207.html</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">5.</span><span style="vertical-align: top;">https://linuxjm.sourceforge.io/html/LDP_man-pages/man5/resolv.conf.5.html</span> ↩</li><li id="fn:3"><span style="vertical-align: top; padding-right: 10px;">6.</span><span style="vertical-align: top;">https://kazmax.zpp.jp/cmd/r/resolv.conf.5.html</span> ↩</li><li id="fn:4"><span style="vertical-align: top; padding-right: 10px;">7.</span><span style="vertical-align: top;">https://manpages.ubuntu.com/manpages/noble/ja/man5/resolv.conf.5.html</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">新年度・新チーム配属のこの季節、「開発環境のセットアップで丸一日溶かした」という経験をされている方も多いのではないでしょうか。特にエンタープライズ企業の「プロキシ・VPN配下」での開発環境構築は、ドキュメント通りに進めても謎のエラーに阻まれがちです。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="Ubuntu" scheme="https://future-architect.github.io/tags/Ubuntu/"/>
    <category term="VPN" scheme="https://future-architect.github.io/tags/VPN/"/>
    <category term="WSL" scheme="https://future-architect.github.io/tags/WSL/"/>
    <category term="トラブルシュート" scheme="https://future-architect.github.io/tags/%E3%83%88%E3%83%A9%E3%83%96%E3%83%AB%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%88/"/>
    <category term="環境構築" scheme="https://future-architect.github.io/tags/%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89/"/>
  </entry>
  <entry>
    <title>S3エミュレーションでrustfsを使ってみたメモとPresigned URLの仕組み</title>
    <link href="https://future-architect.github.io/articles/20260403a/"/>
    <id>https://future-architect.github.io/articles/20260403a/</id>
    <published>2026-04-02T15:00:00.000Z</published>
    <updated>2026-04-02T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>ちょっとしたオブジェクトストレージ前提のシステムのローカルテストでApache 2ライセンスのrustfsを使ってみました。おおむね簡単だったのですが、認証設定をしたり、presigned URLの発行だけちょっと手間がかかってしまったのでその対応とその過程で学んだことのメモです。</p>
<p>このあたり、minioがDockerイメージの配布をやめてメンテナンスモードになったり、LocalStackがユーザー登録必須になってCIで使いにくくなったりでにわかに話題になっていたところですね。</p>
<ul>
<li>さくらんぼの技術備忘録: 手軽に使えるS3互換ストレージを求めて</li>
<li>minioがdockerイメージを配布しなくなったので新しいS3互換ストレージを探す</li>
</ul>
<p>ちょっとしたウェブアプリのバックエンドのストレージとしてオブジェクトストレージが欲しくなったのですが、これまではminioをたまに使ったりしていたものの、別のものを検討するにあたり、docker composeで一緒に起動するという使い方で使いやすいものということで、いろいろ比べてrustfsを選んでみました。</p>
<h2 id="compose-yamlでの利用方法">compose.yamlでの利用方法</h2><p>rustfsの公式イメージをそのまま使うだけです。一瞬で起動します。</p>
<ul>
<li>デフォルトで9000ポートでAPIのエンドポイントを、9001で管理画面(RUSTFS_CONSOLE_ENABLEが必要)を公開します</li>
<li>複数ボリュームのレプリケーションとか色々複雑な機能もありますが、テスト用で可用性はいらなかったので1ボリュームにしています</li>
<li>起動時にはバケットができて欲しいところなので、amazon&#x2F;aws-cliイメージを使って起動時にバケットを作るようにします</li>
</ul>
<figure class="highlight yaml"><figcaption><span>compose.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">rustfs:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">rustfs/rustfs:latest</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">RUSTFS_CONSOLE_ENABLE:</span> <span class="string">&quot;true&quot;</span></span><br><span class="line">      <span class="attr">RUSTFS_ACCESS_KEY:</span> <span class="string">rustfsadmin</span></span><br><span class="line">      <span class="attr">RUSTFS_SECRET_KEY:</span> <span class="string">rustfsadmin</span></span><br><span class="line">      <span class="attr">RUSTFS_VOLUMES:</span> <span class="string">/data/rustfs0</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">rustfs-data:/data</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">rustfs-logs:/logs</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;9000:9000&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;9001:9001&quot;</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;sh&quot;</span>, <span class="string">&quot;-c&quot;</span>, <span class="string">&quot;curl -sS http://localhost:9000/ &gt;/dev/null&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">1s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">5s</span></span><br><span class="line">      <span class="attr">retries:</span> <span class="number">20</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">rustfs-init:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">amazon/aws-cli:2.31.15</span></span><br><span class="line">    <span class="attr">entrypoint:</span> [<span class="string">&quot;/bin/sh&quot;</span>, <span class="string">&quot;-c&quot;</span>]</span><br><span class="line">    <span class="attr">command:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">|</span></span><br><span class="line"><span class="string">        set -eu</span></span><br><span class="line"><span class="string">        until aws --endpoint-url http://rustfs:9000 s3api list-buckets &gt;/dev/null 2&gt;&amp;1; do</span></span><br><span class="line"><span class="string">          sleep 2</span></span><br><span class="line"><span class="string">        done</span></span><br><span class="line"><span class="string">        for bucket in data-bucket log-bucket; do</span></span><br><span class="line"><span class="string">          aws --endpoint-url http://rustfs:9000 s3api create-bucket --bucket &quot;$$bucket&quot; || true</span></span><br><span class="line"><span class="string">        done</span></span><br><span class="line"><span class="string"></span>    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">AWS_ACCESS_KEY_ID:</span> <span class="string">rustfsadmin</span></span><br><span class="line">      <span class="attr">AWS_SECRET_ACCESS_KEY:</span> <span class="string">rustfsadmin</span></span><br><span class="line">      <span class="attr">AWS_REGION:</span> <span class="string">us-east-1</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="attr">rustfs:</span></span><br><span class="line">        <span class="attr">condition:</span> <span class="string">service_healthy</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">rustfs-data:</span></span><br><span class="line">  <span class="attr">rustfs-logs:</span></span><br></pre></td></tr></table></figure>

<p>使い方を調べると、<code>RUSTFS_ADDRESS</code>などの環境変数でアドレスを定義しているものなどもありますが、なくてもデフォルトで9000番（UIは9001番）ポートで開いたので省略しました。</p>
<p>管理画面は動作も軽快だしなかなか良いですね。今まで触ったことのあるウェブを使ったファイル管理画面の中では一番スピードが速くて体験が良いですね。</p>
<img fetchpriority="high" src="/images/2026/20260403a/screenshot_console.png" alt="" width="1137" height="713">

<h2 id="Presigned-URL">Presigned URL</h2><p>これでAWS SDKを使ったデータの読み書きは問題ありませんでしたが、Presigned URLの発行で問題が発生しました。rustfsの問題というかDockerを使っているから起きた問題ですが、rustfsでは、Presigned URLで発行されるURLはリクエスト時のホスト情報をもとに作られます。Dockerの中からは<code>http://rustfs:9000</code>というドメインでアクセスしますが、外からは<code>http://localhost:9000</code>なので、発行されたURLのままではアクセスできないということが起きました。</p>
<p>これは発行時にクライアントを新規で作って、ホストを<code>http://localhost:9000</code>に設定してそれで発行し直す必要がありました。</p>
<figure class="highlight go"><figcaption><span>goのサンプル</span></figcaption><table><tr><td class="code"><pre><span class="line">   <span class="comment">// この環境変数があったらそのホストでURLを発行</span></span><br><span class="line">endpoint := os.Getenv(<span class="string">&quot;RUNTASK_RUSTFS_OBJECT_PUBLIC_ENDPOINT&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> endpoint != <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">	tempOptions := s.options</span><br><span class="line">	tempOptions.Endpoint = endpoint</span><br><span class="line">	tempClient, err := newS3Client(context.Background(), tempOptions)</span><br><span class="line">	<span class="keyword">if</span> err == <span class="literal">nil</span> &#123;</span><br><span class="line">		presigner := s3.NewPresignClient(tempClient)</span><br><span class="line">		presigned, err := presigner.PresignGetObject(context.Background(), &amp;s3.GetObjectInput&#123;</span><br><span class="line">			Bucket: aws.String(s.bucket),</span><br><span class="line">			Key:    aws.String(key),</span><br><span class="line">		&#125;, <span class="function"><span class="keyword">func</span><span class="params">(opts *s3.PresignOptions)</span></span> &#123;</span><br><span class="line">			opts.Expires = expiry</span><br><span class="line">		&#125;)</span><br><span class="line">		<span class="keyword">if</span> err == <span class="literal">nil</span> &#123;</span><br><span class="line">			<span class="keyword">return</span> presigned.URL, <span class="literal">nil</span></span><br><span class="line">		&#125;</span><br><span class="line">		<span class="comment">// fallthrough to try using the existing client</span></span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br><span class="line">   <span class="comment">// 設定がない場合は普通に発行</span></span><br><span class="line">presigner := s3.NewPresignClient(s.client)</span><br><span class="line">presigned, err := presigner.PresignGetObject(context.Background(), &amp;s3.GetObjectInput&#123;</span><br><span class="line">	Bucket: aws.String(s.bucket),</span><br><span class="line">	Key:    aws.String(key),</span><br><span class="line">&#125;, <span class="function"><span class="keyword">func</span><span class="params">(opts *s3.PresignOptions)</span></span> &#123;</span><br><span class="line">	opts.Expires = expiry</span><br><span class="line">&#125;)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">	<span class="keyword">return</span> <span class="string">&quot;&quot;</span>, err</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> presigned.URL, <span class="literal">nil</span></span><br></pre></td></tr></table></figure>

<p><code>Endpoint</code>を上書きしてしまったら逆にバックエンドのサーバーからrustfsに繋がらないからダメなのでは？と思い込んでましたが、このPresigned URLの発行はS3 APIを実際に叩いているわけではなく、SDKの中で発行しているらしい。</p>
<p>使う技術はその名の通り「署名」です。TLSは機密の秘匿化(外から読めない)、完全性保証(改竄検知)、認証(証明書によるサーバーの身元確認)などを行いますが、Presigned URLの場合はこのうちの完全性の保証をベースに、いつ誰が許可したのかの情報が後からわかるようにしています。</p>
<p>サービスにアクセスするのに使うURLに「誰が」というのを明らかにするキーIDと期限が付与されて、シークレットアクセスキーを使って署名されます。署名されているので期限や誰が、といった情報の改ざんは許しません。</p>
<img src="/images/2026/20260403a/screenshot_presigned_url.png" alt="スクリーンショット 2026-03-31 18.26.14.png" width="656" height="361" loading="lazy">

<p>クライアントはそのURLを使ってS3からファイルをダウンロードしたり、ファイルをアップロードします。S3(ここではrustfs)はその署名をみて、改竄されていないことの確認とともに、誰が署名したのかを確認します。ブラウザ自身はクレデンシャルを持っていなくても、その署名をもとにして認可制御が行われ、読み書きが成功するという流れです。</p>
<p><code>Endpoint</code>を書き換えたクライアントを一時的に作るという方針でも、実際にそのクライアントでS3にリクエストを投げることはなくてURLの発行にしか使わないので問題なく利用できるんですね。てっきり、一時的に利用可能なトークン的なURLとして発行されてサーバー側に情報を持っているのかと思いましたが、そんなことはないんですね。勉強になりました。</p>
<h2 id="まとめ">まとめ</h2><p>S3以外もいろいろ必要となる場合は他のAWSエミュレータ（motoとかflociとか）の方が良いかもしれませんが、今回はS3だけが欲しかったのでrustfsを選んでみて使ってみたメモでした。</p>
<p>今まではminioを考えずに使っていましたが、今回別のものを検討してrustfsを使ってみました。seaweedfsとかも良さそうでしたが、filterとかたくさんコンテナが必要そうだったので1つで済むrustfsにしました。コンテナのメモリ消費90MBぐらいですね。動きも軽快なので今後も使ってみようと思いました。</p>
]]></content>
    <summary type="html">ちょっとしたオブジェクトストレージ前提のシステムのローカルテストでApache2ライセンスのrustfsを使ってみました。おおむね簡単だったのですが、認証設定をしたり、presigned URLの発行だけちょっと手間がかかってしまったのでその対応とその過程で学んだことのメモです。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="Docker" scheme="https://future-architect.github.io/tags/Docker/"/>
    <category term="Go" scheme="https://future-architect.github.io/tags/Go/"/>
    <category term="S3" scheme="https://future-architect.github.io/tags/S3/"/>
  </entry>
  <entry>
    <title>実験して入門するKubernetes：Pod起動の裏側を追っていたら、どうしてもSchedulerを止めてみたくなった件</title>
    <link href="https://future-architect.github.io/articles/20260325a/"/>
    <id>https://future-architect.github.io/articles/20260325a/</id>
    <published>2026-03-24T15:00:00.000Z</published>
    <updated>2026-03-24T15:00:00.000Z</updated>
    <author><name>内堀航輝</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2026/20260325a/top.jpg" alt="" width="1024" height="572">

<h2 id="はじめに">はじめに</h2><p>最近、コンテナオーケストレーションの技術領域に興味をもち、Kubernetesを触り始めました。早速自宅で飼ってるMiniPC（ホームサーバ）にArgoCDやLonghornを入れてみたものの、Deployment、Pod、Serviceなど初めましての概念ばかり。気づいたら、AIの指示通りにただコマンド打つマンになっていました。</p>
<p>このままではまずいと思い、Kubernetesの基礎的な挙動を追うことにしました。今回は、どのような流れでPodが作られているのかを実験し、そのアウトプットとしてこの記事にまとめています。</p>
<p>※実験にはMinikubeを使っています。初学者が調べながらまとめたものなので、誤りがあればご指摘ください。</p>
<h2 id="Pod起動までの流れの概要">Pod起動までの流れの概要</h2><p>Podが起動するまでの流れを紹介します。複数のKubernetesコンポーネントが登場しますが、この記事のメインテーマではないため、用語は簡単な説明に留めます。詳細が気になる方は、既存の解説記事や公式ドキュメントをご参照ください。</p>
<figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">kubectl run</span><br><span class="line">  ↓</span><br><span class="line">API Server → etcd（Podデータ保存、ノード未割り当て）</span><br><span class="line">  ↓</span><br><span class="line">Scheduler（最適なノードを割り当て）</span><br><span class="line">  ↓</span><br><span class="line">kubelet（コンテナイメージのPull・起動）</span><br><span class="line">  ↓</span><br><span class="line">podが起動</span><br></pre></td></tr></table></figure>

<ul>
<li><strong>etcd</strong>：Kubernetesの全状態を保存するデータベース</li>
<li><strong>API Server</strong>：全コンポーネントからのリクエスト受付と、状態変更の通知する窓口</li>
<li><strong>Scheduler</strong>：Podをどのノードで動かすかを決定するスケジューラ</li>
<li><strong>kubelet</strong>：各ノード上で実際にコンテナを起動・管理する実行エンジン</li>
</ul>
<blockquote>
<p>補足：<code>kubectl run</code>はPodを直接作成します。Deployment経由の場合は、Controller ManagerがDeploymentデータ → ReplicaSetデータ → Podデータ と各種データを作成するステップが加わります。この記事では、簡略化のため<code>kubectl run</code>による直接的な作成を追いかけます。</p>
</blockquote>
<h2 id="実験1：イベントログでPod作成の流れを確認する">実験1：イベントログでPod作成の流れを確認する</h2><p>最初の実験として、実際に<code>kubectl run</code>を実行し、出力されるイベントログを確認してみます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">% </span><span class="language-bash">kubectl run log-test --image=nginx</span></span><br><span class="line">pod/log-test created</span><br><span class="line"><span class="meta prompt_">% </span><span class="language-bash">kubectl get events -w</span></span><br><span class="line">LAST SEEN   TYPE     REASON      OBJECT         MESSAGE</span><br><span class="line">23m         Normal   Scheduled   pod/log-test   Successfully assigned default/log-test to minikube</span><br><span class="line">22m         Normal   Pulling     pod/log-test   Pulling image &quot;nginx&quot;</span><br><span class="line">22m         Normal   Pulled      pod/log-test   Successfully pulled image &quot;nginx&quot; in 1.481s (1.481s including waiting). Image size: 180545980 bytes.</span><br><span class="line">22m         Normal   Created     pod/log-test   Container created</span><br><span class="line">22m         Normal   Started     pod/log-test   Container started</span><br></pre></td></tr></table></figure>

<p>REASON列とMESSAGE列を見ると、まずScheduledでPodがノード（ここではminikube）に割り当てられ、その後にコンテナの立ち上げ処理が始まっていることがわかります。</p>
<p>さらに<code>kubectl get events -o yaml</code> とオプションを追加すると、各イベントがどのコンポーネントから出力されたのかを詳しく確認できます。</p>
<details>
<summary> kubectl get events -o yaml の出力</summary>

<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="string">%</span> <span class="string">kubectl</span> <span class="string">get</span> <span class="string">events</span> <span class="string">-o</span> <span class="string">yaml</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">items:</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">action:</span> <span class="string">Binding</span></span><br><span class="line">  <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">  <span class="attr">eventTime:</span> <span class="string">&quot;2026-03-17T08:45:27.742811Z&quot;</span></span><br><span class="line">  <span class="attr">firstTimestamp:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">involvedObject:</span></span><br><span class="line">    <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">    <span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62245&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">c79a534d-9c74-45bf-acf4-5a9985c39c14</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">Event</span></span><br><span class="line">  <span class="attr">lastTimestamp:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">message:</span> <span class="string">Successfully</span> <span class="string">assigned</span> <span class="string">default/log-test</span> <span class="string">to</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">metadata:</span></span><br><span class="line">    <span class="attr">creationTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:27Z&quot;</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test.189d9485200842b1</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62247&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">822856bb-0e26-4942-8d78-ac1eff975cb0</span></span><br><span class="line">  <span class="attr">reason:</span> <span class="string">Scheduled</span></span><br><span class="line">  <span class="attr">reportingComponent:</span> <span class="string">default-scheduler</span></span><br><span class="line">  <span class="attr">reportingInstance:</span> <span class="string">default-scheduler-minikube</span></span><br><span class="line">  <span class="attr">source:</span> &#123;&#125;</span><br><span class="line">  <span class="attr">type:</span> <span class="string">Normal</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">  <span class="attr">count:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">eventTime:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">firstTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:28Z&quot;</span></span><br><span class="line">  <span class="attr">involvedObject:</span></span><br><span class="line">    <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">    <span class="attr">fieldPath:</span> <span class="string">spec.containers&#123;log-test&#125;</span></span><br><span class="line">    <span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62246&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">c79a534d-9c74-45bf-acf4-5a9985c39c14</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">Event</span></span><br><span class="line">  <span class="attr">lastTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:28Z&quot;</span></span><br><span class="line">  <span class="attr">message:</span> <span class="string">Pulling</span> <span class="string">image</span> <span class="string">&quot;nginx&quot;</span></span><br><span class="line">  <span class="attr">metadata:</span></span><br><span class="line">    <span class="attr">creationTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:28Z&quot;</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test.189d94853f8ec93d</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62249&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">212f6090-34de-46dd-82eb-d721ace1b32a</span></span><br><span class="line">  <span class="attr">reason:</span> <span class="string">Pulling</span></span><br><span class="line">  <span class="attr">reportingComponent:</span> <span class="string">kubelet</span></span><br><span class="line">  <span class="attr">reportingInstance:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">source:</span></span><br><span class="line">    <span class="attr">component:</span> <span class="string">kubelet</span></span><br><span class="line">    <span class="attr">host:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">Normal</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">  <span class="attr">count:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">eventTime:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">firstTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">involvedObject:</span></span><br><span class="line">    <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">    <span class="attr">fieldPath:</span> <span class="string">spec.containers&#123;log-test&#125;</span></span><br><span class="line">    <span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62246&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">c79a534d-9c74-45bf-acf4-5a9985c39c14</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">Event</span></span><br><span class="line">  <span class="attr">lastTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">message:</span> <span class="string">&#x27;Successfully pulled image &quot;nginx&quot; in 2.135s (2.135s including waiting).</span></span><br><span class="line"><span class="string">    Image size: 180542930 bytes.&#x27;</span></span><br><span class="line">  <span class="attr">metadata:</span></span><br><span class="line">    <span class="attr">creationTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test.189d9485beda4a95</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62252&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">3eb2fd61-7b0b-4f62-b7c3-e5a4be893399</span></span><br><span class="line">  <span class="attr">reason:</span> <span class="string">Pulled</span></span><br><span class="line">  <span class="attr">reportingComponent:</span> <span class="string">kubelet</span></span><br><span class="line">  <span class="attr">reportingInstance:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">source:</span></span><br><span class="line">    <span class="attr">component:</span> <span class="string">kubelet</span></span><br><span class="line">    <span class="attr">host:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">Normal</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">  <span class="attr">count:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">eventTime:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">firstTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">involvedObject:</span></span><br><span class="line">    <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">    <span class="attr">fieldPath:</span> <span class="string">spec.containers&#123;log-test&#125;</span></span><br><span class="line">    <span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62246&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">c79a534d-9c74-45bf-acf4-5a9985c39c14</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">Event</span></span><br><span class="line">  <span class="attr">lastTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">message:</span> <span class="string">Container</span> <span class="string">created</span></span><br><span class="line">  <span class="attr">metadata:</span></span><br><span class="line">    <span class="attr">creationTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test.189d9485c0e87bde</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62253&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">df68a670-7684-4573-beff-b92e5a6bbdec</span></span><br><span class="line">  <span class="attr">reason:</span> <span class="string">Created</span></span><br><span class="line">  <span class="attr">reportingComponent:</span> <span class="string">kubelet</span></span><br><span class="line">  <span class="attr">reportingInstance:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">source:</span></span><br><span class="line">    <span class="attr">component:</span> <span class="string">kubelet</span></span><br><span class="line">    <span class="attr">host:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">Normal</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">  <span class="attr">count:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">eventTime:</span> <span class="literal">null</span></span><br><span class="line">  <span class="attr">firstTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">involvedObject:</span></span><br><span class="line">    <span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line">    <span class="attr">fieldPath:</span> <span class="string">spec.containers&#123;log-test&#125;</span></span><br><span class="line">    <span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62246&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">c79a534d-9c74-45bf-acf4-5a9985c39c14</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">Event</span></span><br><span class="line">  <span class="attr">lastTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">  <span class="attr">message:</span> <span class="string">Container</span> <span class="string">started</span></span><br><span class="line">  <span class="attr">metadata:</span></span><br><span class="line">    <span class="attr">creationTimestamp:</span> <span class="string">&quot;2026-03-17T08:45:30Z&quot;</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">log-test.189d9485c5d61a22</span></span><br><span class="line">    <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line">    <span class="attr">resourceVersion:</span> <span class="string">&quot;62254&quot;</span></span><br><span class="line">    <span class="attr">uid:</span> <span class="string">edf2dbb3-11e1-49e6-bb91-6dbca2d51edb</span></span><br><span class="line">  <span class="attr">reason:</span> <span class="string">Started</span></span><br><span class="line">  <span class="attr">reportingComponent:</span> <span class="string">kubelet</span></span><br><span class="line">  <span class="attr">reportingInstance:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">source:</span></span><br><span class="line">    <span class="attr">component:</span> <span class="string">kubelet</span></span><br><span class="line">    <span class="attr">host:</span> <span class="string">minikube</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">Normal</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">List</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">resourceVersion:</span> <span class="string">&quot;&quot;</span></span><br></pre></td></tr></table></figure>

</details>

<p>出力されたイベントログの <code>reportingComponent</code> フィールドを確認すると、各イベントの出力元は以下です。</p>
<ul>
<li><strong>Scheduled</strong> → <code>reportingComponent: default-scheduler</code></li>
<li><strong>Pulling &#x2F; Pulled &#x2F; Created &#x2F; Started</strong> → <code>reportingComponent: kubelet</code></li>
</ul>
<p>概要で確認した通り、まずSchedulerがノードを割り当てた後、そのノードのkubeletがコンテナの起動処理を行っていることが、実際のログからも確認できました。</p>
<h2 id="Static-PodとMirror-Pod">Static PodとMirror Pod</h2><p>ここまででpodが起動するまでの流れはわかったのですが、ここで1つ疑問が湧きました。</p>
<p>先ほど確認した通り、Podが起動するにはSchedulerによるノード割り当てが必要です。しかし、そのScheduler自身もPodとして動いています。となると、Schedulerを起動するためのSchedulerが必要で、そのSchedulerを起動するためにさらにSchedulerが必要で、、、と無限ループに陥ってしまいます。最初のSchedulerは一体どのように起動されているのでしょうか？</p>
<p>結論から言うと、 SchedulerはAPI Serverや別のSchedulerを経由せず、kubeletがマニフェストファイルから直接起動していました。</p>
<p>実際のSchedulerのマニフェストファイルを確認すると、以下のようになっています。kindが <code>Deployment</code> ではなく <code>Pod</code> となっており、Podデータそのものが定義されていることがわかります。</p>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="string">%</span> <span class="string">minikube</span> <span class="string">ssh</span></span><br><span class="line"><span class="string">$</span> <span class="string">sudo</span> <span class="string">cat</span> <span class="string">/etc/kubernetes/manifests/kube-scheduler.yaml</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Pod</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">labels:</span></span><br><span class="line">    <span class="attr">component:</span> <span class="string">kube-scheduler</span></span><br><span class="line">    <span class="attr">tier:</span> <span class="string">control-plane</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">kube-scheduler</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-system</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">containers:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">command:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">kube-scheduler</span></span><br><span class="line">    <span class="string">...</span></span><br></pre></td></tr></table></figure>

<p>このように、特定のディレクトリにマニフェストを配置し、API Serverを経由せずにkubeletが直接起動・管理するPodを<strong>Static Pod</strong>と呼びます。Kubernetesの起動時には、コントロールプレーンのkubeletがこの仕組みを利用して、API Serverやetcd、Schedulerなどの主要なコンポーネントをStatic Podとして起動しています。</p>
<p>ただ、このStatic PodはAPI Serverを経由しないため、etcd上に実体がありません。そのままでは、<code>kubectl get pods</code>で確認できず、クラスタ全体の状態を一元管理する上で不便です。そこでkubeletは、<strong>Mirror Pod</strong>と呼ばれる読み取り専用のPodを作成します。<code>kubectl get pods -n kube-system</code>で見える<code>kube-scheduler-minikube</code>は、実はこのMirror Podでした。</p>
<h2 id="実験2：Schedulerを止めてみる">実験2：Schedulerを止めてみる</h2><p>ここまでの話が本当なら、以下が成り立つはずです。</p>
<ul>
<li>Mirror Podを削除しても、Schedulerは止まらない（本体はマニフェストファイルだから）</li>
<li>マニフェストファイルを削除（移動）すると、Schedulerが止まる</li>
<li>Schedulerが止まると、新しいPodはノードに割り当てられずPendingのままになる</li>
</ul>
<p>というわけで、さっそく試してみましょう。</p>
<h3 id="1-Mirror-Podを削除する">1. Mirror Podを削除する</h3><p><code>kubectl delete pod</code>で<code>kube-scheduler-minikube</code>を削除してみます。しかし、何度削除しても、即座に復活し、<code>kubectl get pods</code>の結果に出てきます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">% </span><span class="language-bash">kubectl delete pod kube-scheduler-minikube -n kube-system</span></span><br><span class="line">pod &quot;kube-scheduler-minikube&quot; deleted from kube-system namespace</span><br><span class="line"><span class="meta prompt_">% </span><span class="language-bash">kubectl get pods -n kube-system</span></span><br><span class="line">NAME                               READY   STATUS    RESTARTS   AGE</span><br><span class="line">...</span><br><span class="line">kube-scheduler-minikube            1/1     Running   0          5s</span><br><span class="line">...</span><br></pre></td></tr></table></figure>

<h3 id="2-マニフェストファイルを移動する">2. マニフェストファイルを移動する</h3><p>次に<code>minikube ssh</code>でMinikubeのノード内入り、マニフェストファイルを他の場所に動かしてみます（削除すると戻すのが面倒なので<code>mv</code>にしました）。</p>
<p>すると、あれだけ何度削除しても復活していた<code>kube-scheduler-minikube</code>が、<code>kubectl get pods</code>から消えました。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">% </span><span class="language-bash">minikube ssh</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> <span class="built_in">mv</span> /etc/kubernetes/manifests/kube-scheduler.yaml /tmp/</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">exit</span></span></span><br><span class="line"><span class="meta prompt_">% </span><span class="language-bash">kubectl get pods -n kube-system</span></span><br><span class="line">NAME                               READY   STATUS    RESTARTS   AGE</span><br><span class="line">coredns-7d764666f9-bbkvl           1/1     Running   0          11h</span><br><span class="line">etcd-minikube                      1/1     Running   0          11h</span><br><span class="line">kube-apiserver-minikube            1/1     Running   0          11h</span><br><span class="line">kube-controller-manager-minikube   1/1     Running   0          11h</span><br><span class="line">kube-proxy-fj8zg                   1/1     Running   0          11h</span><br><span class="line">storage-provisioner                1/1     Running   0          11h</span><br></pre></td></tr></table></figure>

<h3 id="3-Schedulerなしで新しいPodを作成する">3. Schedulerなしで新しいPodを作成する</h3><p>このSchedulerがいない状態で、新しいPodを作成してみます。Podデータ自体は作成できましたが、STATUSが<code>Pending</code>のまま動かず、NODEも<code>&lt;none&gt;</code>のままです（必要な情報を出力するため<code>custom-columns</code>オプションをつけています）。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">% </span><span class="language-bash">kubectl run new-pod --image=nginx</span></span><br><span class="line">pod/new-pod created</span><br><span class="line"><span class="meta prompt_">% </span><span class="language-bash">kubectl get pods -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,NODE:.spec.nodeName</span></span><br><span class="line">NAME                         STATUS    NODE</span><br><span class="line">hello-node-64fc5894d-57r8s   Running   minikube</span><br><span class="line">hello-node-64fc5894d-7cqrw   Running   minikube</span><br><span class="line">hello-node-64fc5894d-jl4pb   Running   minikube</span><br><span class="line">new-pod                      Pending   &lt;none&gt;</span><br></pre></td></tr></table></figure>

<p>どれだけ待っても<code>Running</code>になることはありませんでした。</p>
<p>Schedulerがいないと、Podデータは作成されても、ノードが割り当てられないため、冒頭で確認したフローのSchedulerのステップで処理が止まってしまうことが実際の挙動でも確認できました。</p>
<h2 id="おわりに">おわりに</h2><p>解説動画や記事を見ても、Kubernetesの各概念やその関係性がピンとこず、「デプロイするとPodが作れてその中にコンテナがいるらしい」くらいの認識でした。そんな状態だったので、当然<code>kubectl</code>コマンドの意味がわかるはずもなく、結果としてAIの指示通りにコマンド打つマンと化していました。</p>
<p>しかし今回、小さな実験を通して手を動かしてみたことで、裏側で各コンポーネントがどのように連携して動いているのかを具体的に追うことができ、一気に理解が深まりました。特にSchedulerは、こうして実験してみなければ存在すら知らなかった概念だったため、非常に学びになりました。</p>
<p>今回の実験で、何度も消されたりファイルを移動させられたりと散々な目に遭わせてしまったSchedulerには、この場を借りて感謝と謝罪を述べて締めくくりたいと思います。</p>
]]></content>
    <summary type="html">Kubernetesの基礎的な挙動を追ってみることにしました。今回は、どういう流れでPodが作られているのかを実験し、そのアウトプットとしてこの記事にまとめています。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="Kubernetes" scheme="https://future-architect.github.io/tags/Kubernetes/"/>
    <category term="Minikube" scheme="https://future-architect.github.io/tags/Minikube/"/>
    <category term="初心者向け" scheme="https://future-architect.github.io/tags/%E5%88%9D%E5%BF%83%E8%80%85%E5%90%91%E3%81%91/"/>
  </entry>
  <entry>
    <title>クラウド世代のITコンサルタントが『Data Center』で物理インフラを体験してみた</title>
    <link href="https://future-architect.github.io/articles/20260319a/"/>
    <id>https://future-architect.github.io/articles/20260319a/</id>
    <published>2026-03-18T15:00:00.000Z</published>
    <updated>2026-03-18T15:00:00.000Z</updated>
    <author><name>片岡久人</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>製造エネルギーグループの片岡久人です。</p>
<p>『Data Center』というゲームがリリースされ、一部界隈で話題になっていたので、プレイした感想をお話できればと思います。</p>
<p>筆者は、普段ITコンサルタントとして、アプリケーション構築やアーキテクチャ設計に携わっていますが、ベースとなるのはGCPなどのクラウド環境がほとんどであり、いわゆるオンプレミス環境でのインフラ構築経験はありません。</p>
<p>ネットワークの理論的な知識は頭に入っているものの、実践的な経験はあまりないというのが現状でした（大学院生時代にシミュレーション用のサーバーラックを組み立てたことはあるのですが、当時は言われるままにネジ留めしていただけで、ITの知識はほぼ皆無でした）。</p>
<p>「普段クラウドでポチポチと構築しているリソースの裏側では、一体どんな物理的な作業が行われているのか？」</p>
<p>そんな知識の隙間を埋めるべく、Steamで配信されているインフラ構築シミュレーションゲーム『Data Center』をプレイしてみました。</p>
<h2 id="『Data-Center』とは？">『Data Center』とは？</h2><p>一言で言えば、「何もない部屋にラックを立て、サーバーをマウントし、LANケーブルを繋いで顧客の要望（タスク）に応えていく」ゲームです。</p>
<p>現在デモ版が無料でプレイ可能です。</p>
<p>■ゲーム情報</p>
<ul>
<li>Steam Store URL：https://store.steampowered.com/app/4376050/Data_Center_Demo/</li>
<li>開発元：Waseku</li>
<li>パブリッシャー：Waseku</li>
</ul>
<img fetchpriority="high" src="/images/2026/20260319a/スクリーンショット_2026-03-15_212110.png" alt="" width="1200" height="676">

<img src="/images/2026/20260319a/スクリーンショット_2026-03-15_204407.png" alt="" width="1200" height="675" loading="lazy">

<p>次々と「このIPサブネットで、これくらいの計算リソース（IOPS）を用意してほしい」という案件が降ってくるので、それに対応するために、機材を発注し、構築していきます。</p>
<p>コンテナで届いた機材を台車で運び、ラックに組み込む。まさに「ゲームの中で仕事（作業）をする」感覚のゲームですが、好きな人にはたまらない没入感があります。</p>
<img src="/images/2026/20260319a/スクリーンショット_2026-03-15_204956.png" alt="" width="1200" height="677" loading="lazy">
<img src="/images/2026/20260319a/スクリーンショット_2026-03-15_205137.png" alt="" width="1200" height="676" loading="lazy">

<h2 id="プレイの所感と学び：理論が「実体験」に変わる瞬間">プレイの所感と学び：理論が「実体験」に変わる瞬間</h2><p>実際にプレイしてみて、普段の業務ではあまり意識しない物理的なインフラ構築の工程を、ゲームの中で疑似的に体験できました。</p>
<h3 id="①-物理的なネットワーク接続のリアル">① 物理的なネットワーク接続のリアル</h3><p>クラウドなら数クリックで終わるネットワークの構築も、物理では当然「ケーブル」が必要です。<br>ゲーム内では、ケーブルリールからLANケーブルを引き出し、スイッチからサーバーへ1本ずつ繋いでいきます。<br><img src="/images/2026/20260319a/スクリーンショット_2026-03-15_210049.png" alt="" width="585" height="818" loading="lazy"></p>
<p>「配線の取り回し（綺麗にまとめないと後で大変なことになる）」や「サーバ種類ごとの配置場所の決定」など、物理ならではの制約を疑似体験できました。</p>
<h3 id="②-IPアドレス計算の手作業">② IPアドレス計算の手作業</h3><p>応用情報の試験勉強で学んだIPアドレスやサブネットマスクの計算。知識としては知っていましたが、実際に顧客（タスク）の要件（&#x2F;28 など）に合わせてIPアドレスを手動でポチポチと設定していく作業は新鮮でした。</p>
<img src="/images/2026/20260319a/スクリーンショット_2026-03-15_211928.png" alt="" width="1200" height="584" loading="lazy">

<p>「あの理論は、現場でこうやって使うためのものだったのか」と、知識が実体験として繋がった感覚がありました。現時点ではCUI（Linuxのターミナル画面など）を叩いて設定するような深さには達していませんが、もしかすると今後そんな要素が出てくるかもしれません（既に存在していたらすいません）。</p>
<h2 id="「仮想化・コンテナ」の偉大さ">「仮想化・コンテナ」の偉大さ</h2><p>今回このゲームをプレイして一番の気付きだったのは、ビジネス視点や技術の歴史の変遷に対する腹落ち感です。</p>
<p>ゲーム中、顧客（タスク）が増えるごとに新しいサーバーを立て、それぞれに計算リソースを割り振り、ネットワーク（IP）を設定し、物理的な配線を繋ぎ変えるという泥臭い作業が発生します。</p>
<p>これを行っているうちに、「現実世界でも昔はこれを毎回人間が手作業でやっていたのか。それは無理があるし、限界が来るな」と感じました。</p>
<ul>
<li>リソースが無駄に余っていても、物理的に繋がっていなければ他の顧客に使い回せない</li>
<li>構成変更のたびに、データセンターに行ってケーブルを挿し直さなければならない</li>
</ul>
<p>この「物理的なインフラ管理の限界と苦労」を身をもって体験したことで、それを解決するために生まれた「仮想化」や「コンテナ化」、そして「クラウド（IaaS）」といった技術がいかに偉大で、ビジネスのスピードアップに不可欠なものだったかが、身をもって理解できました。</p>
<p>物理の不便さを知ることで、現在私たちが当たり前のように使っている抽象化された技術のありがたみと、その進化の必然性を強く再認識できたのは、ITコンサルタントとして大きな収穫でした。</p>
<h2 id="おわりに">おわりに</h2><p>『Data Center』は、最初はひたすらサーバーを立ててLANを繋ぐという「作業ゲー」の側面が強いですが、インフラの裏側を知りたい、あるいはクラウド技術の成り立ちを逆接的に体感したいエンジニアには、気づきを与えてくれるゲームだと思いました。</p>
<p>普段クラウド上で何気なくプロビジョニングしているリソースの裏には、こうして稼働している物理サーバー群がある。その事実を想像できるようになっただけでも、今後のアーキテクチャ設計に少し深みが出せそうです。刺さる人には間違いなく刺さるゲームなので、気になった方はぜひプレイしてみてください。</p>
]]></content>
    <summary type="html">今回は『Data Center』というゲームがリリースされ、一部界隈で話題になっていたので、プレイした感想をお話できればと思います。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="オンプレミス" scheme="https://future-architect.github.io/tags/%E3%82%AA%E3%83%B3%E3%83%97%E3%83%AC%E3%83%9F%E3%82%B9/"/>
    <category term="ゲーム" scheme="https://future-architect.github.io/tags/%E3%82%B2%E3%83%BC%E3%83%A0/"/>
    <category term="ネットワーク" scheme="https://future-architect.github.io/tags/Network/"/>
  </entry>
  <entry>
    <title>メール設計ガイドラインを公開しました</title>
    <link href="https://future-architect.github.io/articles/20251003a/"/>
    <id>https://future-architect.github.io/articles/20251003a/</id>
    <published>2025-10-02T15:00:00.000Z</published>
    <updated>2025-10-02T15:00:00.000Z</updated>
    <author><name>藤井亮佑</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは。TIG-DXの藤井です。</p>
<p>当社の有志メンバーの協力により、アーキテクチャ設計ガイドラインというものを作成・公開しています。</p>
<p>各分野におけるアーキテクチャ設計の指針となるべく作成されているガイドライン集ですが、この度、新たにメール設計ガイドラインを作成しました。</p>
<p>本記事では、このメール設計ガイドラインについて、内容にも触れつつご紹介をしていきます。</p>
<h2 id="メール設計ガイドライン概要">メール設計ガイドライン概要</h2><p>皆さん、普段からメールは利用されているかと思います。その手軽さ・利用者の多さから、純粋なコミュニケーションツールとしてはもちろん、webシステムにおけるユーザ認証や、販促・各種取引など、多岐にわたって活用されています。</p>
<p>一方、実際にメールという通信手段がどのように実現されているのか・どういった技術が利用されているのか、等を意識することはあまり無いのではないでしょうか。<br>当然のことではありますが、メール送受信の裏側にはそれを実現しているシステムが存在しています。そういったシステムが正常に稼働しなくなった場合に生じる影響の大きさは、想像に難くないでしょう。</p>
<p>しかしながら、現実問題としてメールに関するシステムトラブルは決して珍しいものではありません。利用者・用途が多いだけに、その分トラブルが起きる余地も大きいのです。</p>
<p>本ガイドラインでは、メール送信を伴うシステムを構築する際に、どのようにシステムを設計すれば安定してメール送信を行うことができるか・ハマりどころは何か、といった内容を整理し、推奨事項に落とし込んでいます。一通り目を通すことで、システムにおけるメール設計の肝が理解できるようなガイドラインを目指して作成しています。</p>
<p>逆に、SMTPそのものの仕様等の、アーキテクチャ設計に直結しない内容については省略しています。メルマガなどの手動送信されるメールや、システム監視等におけるメール通知なども、部分的に参考にできるところはあるかと思いますが、基本的にはスコープ外としています。</p>
<p>あくまで、システムがユーザにメールを送信するためのシステム設計を対象としていることを前提に読んでいただければと思います。</p>
<h2 id="コンテンツ紹介">コンテンツ紹介</h2><p>大前提として考慮すべき要素から、メールを取り扱う上で必要な事柄まで幅広く盛り込んだ結果、公開済の他のガイドラインに見劣りしない分量となりました。</p>
<p>メールについて書くだけでそんな量を？と思われるかもしれませんが、実はそれだけ考慮しなければいけないことの量と質が求められるのが、メールという分野であると考えています。</p>
<p>とは言え、ここで全てをご紹介できないため、一部を抜粋の上、簡略化してお伝えしようと思います。興味を持って頂けた・より詳しく知りたい方は、ぜひメール設計ガイドラインをご覧ください。</p>
<h3 id="なぜメール送信は難しいのか">なぜメール送信は難しいのか</h3><p>いきなりガイドラインには（明示的には）記載していない内容なのですが、そもそも何故メールを送信することが難しいのでしょうか。送信プロトコルであるSMTPをはじめとして、技術自体はかなり古く、高度すぎるものではないはずです。</p>
<p>この問への回答は単純で、<strong>スパムメールが存在するせい</strong>です。メールの歴史は正当な送信者やプロバイダと、スパムメールを送信する悪質な送信者との戦いの歴史と言っても過言ではありません。</p>
<p>先述の通り、メールは利用者が多く、その用途は多岐にわたります。そのため、全ての受信者に対して高いリテラシーを要求できず、またその手軽さから悪用のハードルも低くなっています。そのような状況を踏まえ、メールプロバイダは受信者を悪意ある送信者から守る必要があると考え、スパムであると判定したメールを受信者に届けず処分するという選択を取っています。しかしながら、そのようなスパム判定はもちろん機械的に行わざるを得ないため、（受信者にとっては）正当なメールもスパムメールであると判定してしまい、結果届かなくなるということが起こるのです。</p>
<p>（代表的なプロバイダの動向として、2024年に施行された、Gmailにおけるメール送信者のガイドラインがあります。Gmailが求めるガイドラインに準拠しない送信者からのメールはブロックする（可能性がある）としたもので、多くのシステムが準拠のための対応をすることになりました。）</p>
<p>そのため、（正当な）メール送信者側は、プロバイダに「これはスパムメールではない」と判断してもらうため、適切な対応をとる必要があります。</p>
<h3 id="ドメイン認証">ドメイン認証</h3><p>第一の手立てとして、送信者が送信元メールアドレスに記載されているドメインの所有者であることを確認する、<strong>ドメイン認証</strong>があります。ドメイン認証を1つも行っていない場合、ほとんどのケースでスパム認定がされてしまうため、必ず実施する必要があります。</p>
<p>スパムメールの送信者は、メールが正当なものであると誤認させるために、送信元メールアドレスのドメインを（社会的信頼のある企業などの）ドメインに書き換えてメールを送信することがあります。これはSMTPの仕様として認められており、極めて容易に実現できます。これを<strong>ドメインのなりすまし</strong>と呼びます。受信者がなりすまされたドメインのみを確認し、メールを信用してしまえば、フィッシング詐欺などの被害にあってしまう可能性があります。これを防ぐため、ドメインのなりすましを機械的に検知できる仕組みとしてドメイン認証が導入されました。</p>
<p>ここでは詳細は割愛しますが、SPF&#x2F;DKIM&#x2F;DMARCと呼ばれる3種の仕組みがあり、いずれも本来は名前解決に用いるDNSを活用した仕組みです。ドメイン認証が成功するメールを送信するためには、DNSに適切なレコードを登録できる人物 &#x3D; ドメインの所有者でなくてはならない、というロジックで送信元の正当性を担保しています。</p>
<img fetchpriority="high" src="/images/2025/20251003a/image.png" alt="image.png" width="1200" height="488">

<p>ただしこのドメイン認証は、スパムメールを防ぐための機構として完全なものではありません。</p>
<p>ガイドライン上ではTipsとして記載していますが、正規のドメインに酷似していたり、タイプミス等で誤入力しやすい、いわゆるドッペルゲンガードメインと呼ばれるドメインを保有している人物が当該ドメインを攻撃に利用した場合、ドメイン認証は通過してしまいます。ドメイン認証はあくまで、送信者がドメインを所有していることを保証するのみで、送信者自身の正当性を担保するものではないのです。（冒頭記載の通り、ドメイン認証すら通過しない送信者を不正と断じてしまえるため、完全に意味がないということはもちろんありません。）</p>
<h3 id="レピュテーション">レピュテーション</h3><p>ドメイン認証の結果が必ずしも送信者の正当性を担保しないことを踏まえ、メールプロバイダは別の方針で送信者を評価し、ドメイン認証の結果と合わせてメールの取り扱いを判断しています。それが<strong>レピュテーション</strong>と呼ばれるもので、言葉通りメール送信者の「評判」を意味します。レピュテーションが十分な送信者からのメールは通常通り受信者に配信し、レピュテーションが低い送信者からのメールはスパムメールであると判断し処分してしまう、という単純な仕組みです。</p>
<p>そのため送信者としては、<strong>レピュテーションを向上し、高い状態を維持する</strong>ことが安定したメール送信のために取れる手立ての2つ目となります。このレピュテーションは主に、送信元のIPアドレスとドメインに対して評価されます。ドメイン認証の結果と合わせて見ると、<strong>レピュテーションが高いドメインの所有者が送信したメール</strong>であれば高い確率で正常に配信されることが期待されます。</p>
<p>このレピュテーションは、メールの送信状況や受信者のアクション（迷惑メール報告等）により、プロバイダごとに常に変動して管理がされるため、送信状況の監視・適切なコンテンツの作成が安定したメール配信に強く影響します。</p>
<img src="/images/2025/20251003a/image_2.png" alt="image.png" width="1200" height="424" loading="lazy">

<h3 id="自作メールサーバ-vs-メール送信SaaS">自作メールサーバ vs メール送信SaaS</h3><p>メール送信を実現する場合、大きく分けて「メールサーバを自作する」・「メール送信用のSaaSを利用する」の二方針に分類されますが、ガイドラインでは圧倒的に<strong>SaaSの利用を推奨</strong>しています。SaaSを利用することで、各種設定の簡易化・送信状況等の監視機能が利用可能となる・（IP）レピュテーションがある程度担保されている等のメリットを享受できるためです。</p>
<p>ここまでに記載したドメイン認証・レピュテーションの管理ともに、適切な設定・運用は容易ではありません。その一方で、設定誤りや運用ミスが有った場合の影響は極めて大きい（最悪の場合、一切のメールが宛先に届けられない）です。このようなリスクがSaaSの利用により極小化できるのです。</p>
<p>もちろん利用コストが生じることや、（レピュテーションを低下させる行動を繰り返したなどにより）SaaSの利用が停止されてしまうなどのリスクは存在しますが、それを補って余りあるメリットであると考えています。</p>
<h3 id="コンテンツの工夫">コンテンツの工夫</h3><p>レピュテーションには、受信者のアクション（迷惑メール報告等）が影響すると記載しました。つまり、受信者にとってスパムであるかのように映るメールを送信するとレピュテーションが下がり、結果としてメール未達などに繋がってしまいます。そのため、システム面のみならず、実際に送信されるメールの内容についても工夫が必要です。</p>
<p>ガイドラインでは、以下要素などが工夫すべきポイントとしています。</p>
<ul>
<li>件名: 要件が正しく伝わり、優良誤認とならない範疇で簡潔にする。</li>
<li>本文: マルチパート配信とする。css等による装飾は過剰となりすぎないようにする。</li>
<li>送信者名: 送信者・目的が分かるものとする。</li>
<li>送信元メールアドレス: info, noreply等、メールの性質が伝わるアドレスにする。</li>
</ul>
<h3 id="トラブルシューティング">トラブルシューティング</h3><p>ここまでに記載したこと、およびガイドラインに記載したことに準拠していても、突然メールが届かない、といった事態が発生することは想定されます。これは送信者・プロバイダ・受信者という3者が関わる仕組みである以上、確実に回避できるものではありません。</p>
<p>メール送信が正常に行えないという状況自体、影響として大きなものですが、誤った対応や放置をしてしまうと状況は更に悪化してしまいます。</p>
<p>基本的には継続的な問題が確認された時点でメール送信機能は停止し、状況を把握した上で対処を開始することを推奨しています。ガイドラインには具体的な調査するべき内容等も記載しているので、合わせてご確認ください。</p>
<h2 id="おわりに">おわりに</h2><p>先述の通り、メール送信を伴うシステムは一度構築したら終わりというものではなく、継続的な監視・適切な運用が必要なものとなっています。構築・運用方針を誤ってしまうと、最悪の場合メール配信が行えなくなってしまいます。</p>
<p>今回作成したガイドラインには、本記事では触れられなかった（量・質ともに）内容も多く記述されています。ぜひ通しでご覧いただき、安定したメールシステムの構築に活用いただければと思います。</p>
<p>最後になりますが、ガイドラインへのご意見や訂正PRなどは広く募集しております。目を通していただいて、疑問に思った点・誤り等ありましたらご意見いただけますと幸いです。</p>
<ul>
<li>GitHub</li>
<li>ガイドライン一覧</li>
</ul>
<p>今後も継続してガイドラインは作成していく予定です。既存・新規問わず、アーキテクチャ設計の一助となることを願っております。</p>
]]></content>
    <summary type="html">有志メンバーの協力により、アーキテクチャ設計ガイドラインを作成しました。本記事では、このメール設計ガイドラインについて、内容にも触れつつご紹介をしていきます。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="ガイドライン" scheme="https://future-architect.github.io/tags/%E3%82%AC%E3%82%A4%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3/"/>
    <category term="メール" scheme="https://future-architect.github.io/tags/%E3%83%A1%E3%83%BC%E3%83%AB/"/>
  </entry>
  <entry>
    <title>TCP Proxyを作って面倒なProxy設定を一掃する ～Rust製moproxyとnftablesによる透過プロキシ設定～</title>
    <link href="https://future-architect.github.io/articles/20250904a/"/>
    <id>https://future-architect.github.io/articles/20250904a/</id>
    <published>2025-09-03T15:00:00.000Z</published>
    <updated>2025-09-03T15:00:00.000Z</updated>
    <author><name>神崎 林太郎</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250904a/top.jpg" alt="" width="1024" height="1024">

<p>夏の自由研究2025ブログ連載の8日目です。</p>
<h2 id="はじめに">はじめに</h2><p>Technology Innovation Groupの神崎です。</p>
<p>セキュリティ上の理由などで、どうしてもProxyを使わざるを得ない環境下の時に、非常にハマりがちな設定のひとつとしてProxyの設定があります。これには理由があり、ProxyというのはOSレベルで適用するものではなく、利用するツール毎に設定する必要があり、各ツールのドキュメンテーションを読んで適切な設定をする必要があります。そのため利用したいツールすべてについて、適切な設定をしないと動かないという特徴があります。「新しく使い始めたCLIツールがプロキシに対応していなかった」「Dockerのpullがプロキシで失敗する」などあるあるではないかと思います。</p>
<p>また、Proxy機能はツール目線でいうとよく使われる機能ではないため、特に「枯れていない」ツールを利用する際は、そもそもドキュメンテーションがなかったり、Proxyの認証に関してバグがあったりなど、Proxyの設定のトラブルシューティングに非常に時間がかかることがあります（場合によってはツールのソースコードを追うようなことまで必要になります）。</p>
<p>これらは非常に時間がかかり、生産性を低下させることが多く、特にツールの選定やちょっと使ってみたい場合はそれ自体を諦める遠因となることさえあります。そのため、何かツールごとではなく、OSレベルで設定する方法があれば、快適に過ごせるのではないか？と思っていました。</p>
<h2 id="TL-DR">TL;DR</h2><p>解決策として、下記の構成を提案します。</p>
<ul>
<li>moproxyを利用し、TCPパケットをHTTP CONNECTリクエストにくるんで、Proxyへ中継する</li>
<li>Linuxカーネルのパケットフィルタリング機能 <sup id="fnref:1">1</sup> であるnftablesを利用して、宛先ポートが80(HTTP)ないし443(HTTPS)へのパケットをmoproxyに転送する<ul>
<li>Well known portsではないポートへの通信は一旦ないものと想定する（必要に応じて個別に設定する）</li>
</ul>
</li>
<li>各ツールでは、Proxyの設定は全く行わず、通常のProxyなしの設定を行い利用する</li>
</ul>
<p>本記事ではその構成でなぜ動くのかを、HTTPのCONNECT仕様を深掘って解説した上で、具体的な設定例を示します。</p>
<p>※今回の記事ではWindows上のアプリケーションについては対象としていません。特にVPNクライアントが入っているケースを踏まえるとパケットがループしないようにうまく工夫をする必要がありますが、私自身がWindowsに明るくないので、Linuxのみの記述になります。すみません(多くのアプリケーションではインターネットオプションの設定を読みに行く形になるため、そこまで気にする機会はないかなと思います。<del>だが、<code>Invoke-WebRequest</code>、おまえはゆるさん…</del>)。</p>
<h2 id="そもそもProxyとの通信は何をやっているのか">そもそもProxyとの通信は何をやっているのか</h2><p>私が一から解説する必要はないと思いますので、詳しくはこのサイト(作って学ぶ 「Https Man in The Middle Proxy」 in Go)を確認して貰いたいのですが、大きく分けると以下の2つのことをやっています。(HTTPSを前提とした場合)</p>
<ol>
<li>HTTP CONNECTでProxyにHTTPS通信を始めたい旨を通知する(この情報を元にProxyは②のパケットを転送する)</li>
<li>ProxyにTCPパケットそのものを転送する</li>
</ol>
<pre class="mermaid" data-mermaid="5f1368aa22ac71f92ca1e2f4e069d316dc4409aaf6f9bd5ede3e4268eebdd3ab">sequenceDiagram
    participant Browser as ブラウザ/ツールなど
    participant Proxy as Proxy
    participant Web as 接続先

    Note over Browser,Proxy: ①Proxyに通信を始めたい旨を通知する
    Browser->>+Proxy: HTTP CONNECT
    activate Browser
    Proxy->>Proxy: 認証
    Proxy->>-Browser: 200 or 407 RESPONSE

    Note over Browser,Proxy: ②パケット転送を開始する
    Browser->>+Proxy: TCPパケットの送信
    Proxy->>+Web: TCPパケットの転送
    Web->>Web: 返却
    Web->>-Proxy:
    opt
        Proxy->>Proxy: 復号化/再暗号化
    end
    Proxy->>-Browser:
    deactivate Browser</pre>

<h2 id="Proxyを「透過」させるのに必要な実装">Proxyを「透過」させるのに必要な実装</h2><p>前述した仕組みを踏まえると、HTTPS通信にてProxy処理を「透過的」に行うには、ブラウザ&#x2F;ツールなどから何かしらの手段でTCPのパケットを横取りし、パケットを送信する前にProxyにCONNECTリクエストを送った上で、横取りしたTCPパケットを再送してあげればよいことになります。<br>（また、HTTP通信の場合でも、相手先のProxyの実装にもよりますが、同様にTCPパケットを転送することで透過的に扱うことができます）</p>
<p>Linux上では、だいたい以下のような設定をすることで上記を実現できます。</p>
<pre class="mermaid" data-mermaid="d23c62c6d630dcd451d3c97cd1128e21bb93bcee051f4f8dac1c6df5fb709cfc">flowchart TB
Web[接続先]
Browser[ブラウザ/ツールなど] --> nftables
subgraph nftables
    direction TB
    decision1{内部IPへの通信か？}
    decision1 -->|No| decision2
    decision2{port 443 or 80への通信か?}
end
decision1 -->|Yes| Web
decision2 -->|Yes| moproxy
decision2 -->|No| Web
moproxy --> Web</pre>

<h3 id="moproxyの設定例">moproxyの設定例</h3><p>moproxyの設定例を示します。WSL2上のubuntuを前提に書いているため適宜読み替えて貰えればと思います。</p>
<p>今回はProxyがTLSの復号・再暗号化を行うので、必要な中間証明書をインストールします。<br>※信頼できる証明書をインストールするように注意してください！</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> install -m 644 証明書パス.crt /usr/local/share/ca-certificates/internal_ca.crt</span><br><span class="line"><span class="built_in">sudo</span> update-ca-certificates</span><br></pre></td></tr></table></figure>

<p>moproxyをインストールします。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">TMPDIR=$(<span class="built_in">mktemp</span> -d)</span><br><span class="line">curl -fL -o <span class="string">&quot;<span class="variable">$&#123;TMPDIR&#125;</span>/moproxy.deb&quot;</span> <span class="string">&#x27;https://github.com/sorz/moproxy/releases/download/v0.5.1/moproxy_0.5.1-1_amd64.deb&#x27;</span></span><br><span class="line"><span class="built_in">sudo</span> dpkg -i <span class="string">&quot;<span class="variable">$&#123;TMPDIR&#125;</span>/moproxy.deb&quot;</span></span><br></pre></td></tr></table></figure>

<p>下記ファイルを環境によって読み替えながら設置します。</p>
<figure class="highlight toml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># /etc/systemd/system/moproxy.service.d/override.conf</span></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">EnvironmentFile</span>=</span><br><span class="line"><span class="attr">EnvironmentFile</span>=/etc/default/moproxy</span><br><span class="line"><span class="attr">ExecStart</span>=</span><br><span class="line"><span class="attr">ExecStart</span>=/usr/bin/moproxy --host <span class="variable">$HOST</span> --port <span class="variable">$PORT</span> --list /etc/moproxy/proxy.ini</span><br></pre></td></tr></table></figure>

<figure class="highlight toml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># /etc/moproxy/proxy.ini; chmod 600 推奨</span></span><br><span class="line"><span class="section">[server-1]</span></span><br><span class="line"><span class="attr">address</span>=proxy.fqdn</span><br><span class="line"><span class="attr">protocol</span>=http</span><br><span class="line">http <span class="attr">username</span> = username</span><br><span class="line">http <span class="attr">password</span> = passw0rd</span><br></pre></td></tr></table></figure>

<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="comment"># /etc/default/moproxy</span></span><br><span class="line">HOST=<span class="string">&quot;::&quot;</span></span><br><span class="line">PORT=<span class="string">&quot;2080&quot;</span></span><br></pre></td></tr></table></figure>

<p>最後にsystemdを設定します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl daemon-reload</span><br><span class="line">systemctl is-enabled moproxy.service || <span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> moproxy.service</span><br><span class="line"><span class="built_in">sudo</span> systemctl is-active moproxy.service &amp;&amp; <span class="built_in">sudo</span> systemctl restart moproxy.service || <span class="built_in">sudo</span> systemctl start moproxy.service</span><br></pre></td></tr></table></figure>

<p>上記にて、port 2080で待ち受けるサービスが作成されます。</p>
<h3 id="nftablesでの設定例">nftablesでの設定例</h3><p>nftables自体はカーネルの機能としてインストールされているため、systemd-unitファイルを用意します。<br>特にDockerを利用している場合は、デフォルトで用意されている<code>nftables.service</code>を使うと、Docker用のネットワーク設定がリセットされてしまうため、個別に<code>nft</code>コマンドを呼ぶようにします。</p>
<p>unitファイルは、<code>nft</code>コマンドを直接呼びdaemonが起動しないので、<code>Type=oneshot</code>を設定します。また、<code>moproxy.service</code>への依存関係を持たせたいため、<code>RemainAfterExit=yes</code>をセットします。</p>
<p>nftables上では以下のルールを設定します。</p>
<ul>
<li><code>proxy</code>という名称の新規tableを作成</li>
<li><code>proxy</code> tableにpreroutingのnat chainを追加 ※ルーティングされてきたパケット用、Dockerなど</li>
<li><code>proxy</code> tableにoutputのnat chainを追加 ※ホストからのパケット用</li>
<li>内部IPの場合は<code>accept</code>し何もしない (下記例だとクラスAアドレス)</li>
<li>tcpの宛先ポートが80,443の場合は、port 2080にパケットを転送する</li>
</ul>
<figure class="highlight toml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># /usr/local/lib/systemd/system/nft-proxy.service</span></span><br><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=nftables proxy</span><br><span class="line"><span class="attr">Wants</span>=moproxy.service</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=<span class="literal">on</span>eshot</span><br><span class="line"><span class="attr">RemainAfterExit</span>=<span class="literal">yes</span></span><br><span class="line"><span class="attr">ExecStart</span>=/usr/sbin/nft add table proxy</span><br><span class="line"><span class="attr">ExecStart</span>=/usr/sbin/nft add chain ip proxy prerouting <span class="string">&#x27;&#123;type nat hook prerouting priority 10;&#125;&#x27;</span></span><br><span class="line"><span class="attr">ExecStart</span>=/usr/sbin/nft add chain ip proxy output <span class="string">&#x27;&#123;type nat hook output priority 10;&#125;&#x27;</span></span><br><span class="line"><span class="attr">ExecStart</span>=/usr/sbin/nft add rule proxy output ip daddr <span class="number">10.0</span>.<span class="number">0.0</span>-<span class="number">10.255</span>.<span class="number">255.255</span> accept</span><br><span class="line"><span class="comment"># ExecStart=/usr/sbin/nft nft add rule proxy output ip daddr &lt;proxy_ip&gt; accept</span></span><br><span class="line"><span class="attr">ExecStart</span>=/usr/sbin/nft add rule proxy output tcp dport &#123;<span class="number">80</span>, <span class="number">443</span>&#125; redirect to <span class="number">2080</span></span><br><span class="line"><span class="attr">ExecStart</span>=/usr/sbin/nft add rule proxy prerouting tcp dport &#123;<span class="number">80</span>, <span class="number">443</span>&#125; redirect to <span class="number">2080</span></span><br><span class="line"><span class="attr">ExecStop</span>=/usr/sbin/nft flush table proxy</span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br></pre></td></tr></table></figure>

<p>※今回はProxyサーバが80でも443でもないポートを利用していたので、特にループしていませんが、Proxyサーバが80か443を利用している場合は宛先IPによる<code>accept</code>ルールを追加する必要があります。</p>
<h3 id="WSLでの設定例">WSLでの設定例</h3><p>WSL上で自動的にプロキシ設定しないよう、wsl.confを設定します。</p>
<figure class="highlight toml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># %USERPROFILE%\.wslconfig</span></span><br><span class="line"><span class="section">[wsl2]</span></span><br><span class="line"><span class="attr">autoProxy</span> = <span class="literal">false</span></span><br><span class="line"><span class="attr">dnsTunneling</span> = <span class="literal">true</span></span><br></pre></td></tr></table></figure>

<h3 id="注意点">注意点</h3><p>今回の構成では力業でTCPパケットを転送するということを行っているため、当然UDPを利用する通信(QUICやDNS)はProxyにリダイレクトされないです。</p>
<p>特にProxyへの除外先をIPではなくFQDN名で指定したいというニーズがある場合、この構成ではうまく動きません。moproxyの設定ファイルにはFQDN名でルール設定できるように見えますが、上記の構成ではmoproxy自体はTLSを復号しないため、FQDNを読む手段がなく動きません。(相手先のProxyがsocksv5などに対応していて、moproxyでCNIを読める設定にすれば可)</p>
<p>やはりProxyを使わざるを得ない環境では実現できる構成に制限がつくということかなと思います。</p>
<h3 id="小ネタ">小ネタ</h3><p>今回の設定では必要ないですが、Proxyへの認証時にはユーザー名やパスワードのURLエンコードが必要になるケースがあります。jqでは<code>@uri</code>を使うことでワンライナーでURLエンコードをやってくれるので、紹介しておきます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 例</span></span><br><span class="line">PROXY_PASSWORD=$(systemd-ask-password --keyname=proxy.password <span class="string">&quot;プロキシログイン用パスワードを入力してください: &quot;</span>)</span><br><span class="line">PROXY_PASSWORD=$(<span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;PROXY_PASSWORD&#125;</span>&quot;</span> | jq -Rr @uri)</span><br><span class="line">https_proxy=<span class="string">&quot;http://<span class="variable">$&#123;PROXY_USERNAME&#125;</span>:<span class="variable">$&#123;PROXY_PASSWORD&#125;</span>@<span class="variable">$&#123;PROXY_URL&#125;</span>&quot;</span> curl -fL -o ...</span><br></pre></td></tr></table></figure>

<h2 id="さいごに">さいごに</h2><p>Proxyを介したコネクションはツールの実装によるところがあり、トラブルが起こりやすいですが、この方式であればツール側はProxyの存在を気にせずに通常通りのリクエストを行うことができます。実は、Rancher Desktopは内部で同じ仕組みを使っており、いい仕組みを考える人がいるもんだと感心をしました。</p>
<p>また、moproxyはRustで書いてあり、動作を理解するために今回初めてまともにRustのソースコードを読んだのですが、とてもよい勉強になりました。自分で一から書けるかは置いておいて、読むのに当たっては結構好きな言語かもしれないという発見もあったので、機会があれば自分で書いたりもできればと思います。（特にマイコン向けとかで低レイヤーの実装が必要になった際には挑戦してみたい気がします）</p>
<h2 id="参考">参考</h2><p>先人たちも色々な工夫をされていたようなので、こちらも参考にしてみてください。</p>
<ul>
<li>ProxyとDockerと新人社員と時々わたし</li>
<li>ローカルプロキシで認証プロキシの煩わしさを解消！</li>
</ul>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">NATの作成や、FWのルール設定などのバックエンドとして機能する</span> ↩</li></ol></div></div>

]]></content>
    <summary type="html">セキュリティ上の理由などで、どうしてもProxyを使わざるを得ない環境下の時に、非常にハマりがちな設定のひとつとしてProxyの設定があります。これには理由があり、ProxyというのはOSレベルで適用するものではなく、利用するツール毎に設定する必要があり、各ツールのドキュメンテーションを読んで適切な設定をする必要があります。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="Linux" scheme="https://future-architect.github.io/tags/Linux/"/>
    <category term="Rust" scheme="https://future-architect.github.io/tags/Rust/"/>
    <category term="WSL" scheme="https://future-architect.github.io/tags/WSL/"/>
    <category term="ネットワーク" scheme="https://future-architect.github.io/tags/Network/"/>
    <category term="プロキシ" scheme="https://future-architect.github.io/tags/%E3%83%97%E3%83%AD%E3%82%AD%E3%82%B7/"/>
    <category term="環境構築" scheme="https://future-architect.github.io/tags/%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89/"/>
  </entry>
  <entry>
    <title>Software Design 2025年8月号「2038年問題を考える」を寄稿しました</title>
    <link href="https://future-architect.github.io/articles/20250718a/"/>
    <id>https://future-architect.github.io/articles/20250718a/</id>
    <published>2025-07-17T15:00:00.000Z</published>
    <updated>2025-07-17T15:00:00.000Z</updated>
    <author><name>星名藍乃介</name></author>
    <content type="html"><![CDATA[
<img fetchpriority="high" src="/images/2025/20250718a/image.png" alt="" width="800" height="1130">


<h2 id="はじめに">はじめに</h2><p>こんにちは、2025年4月 新卒入社、Cyber Security Innovation Group (CSIG) 所属の星名 藍乃介（HOSHINA Rannosuke）です。</p>
<p>技術評論社様（以下敬称略）の Software Design 2025年8月号　特別企画「2038年問題を考える」に寄稿する機会をいただきました。</p>
<p>Software Design 2025年8月号は 2025年7月17日発売ですので、ぜひご覧いただければ幸いです。</p>
<p>今回は、「2038年問題を考える」コラムに寄稿させていただいたことへの思いと、入社4か月目というフレッシュな視点から執筆活動に対する思いについてお伝えできたらと思います。</p>
<h2 id="Software-Design-とは">Software Design とは</h2><p>「Software Design」は、技術評論社が発行している、プログラミングやOS、Web技術など、ソフトウェア開発全般に関する情報を提供する月刊誌です。</p>
<p>プログラミングの具体的なテクニックから、OS、Web技術、データベース、ネットワーク、セキュリティといった幅広いITの基礎知識、さらには最新の開発手法やチームマネジメントに至るまで、多岐にわたるテーマを深く掘り下げて解説しています。</p>
<p>単なる情報の羅列ではなく、実際に現場で役立つ実践的なノウハウや、技術の背景にある設計思想、考え方に焦点を当てているのが大きな特徴です。</p>
<h2 id="Software-Design「2038年問題を考える」寄稿に対する思い">Software Design「2038年問題を考える」寄稿に対する思い</h2><p>今回、Software Designという多くの方に愛読されている雑誌で2038年問題についてお伝えする機会をいただき、大変光栄に思っています。</p>
<p>このテーマは、私が大学時代から関心を持ち、その脅威を広く知ってもらいたいと活動を続けてきたものです。</p>
<h3 id="2038年問題とは">2038年問題とは</h3><p>2038年問題は、UNIXやC言語に関わるシステムにおいて2038年のある時刻以降の時刻を表現できなくなり、その結果として様々な不具合が生じうる問題です。</p>
<p>コラムでも一部の脅威性評価の結果に言及していますが、影響範囲はいまだ不明瞭な部分が多く、社会全体で協力して解決に取り組むべき重要な課題だと考えています。</p>
<h3 id="寄稿の経緯">寄稿の経緯</h3><p>私はこの問題の深刻さを以前から認識しており、大学院当時から情報処理学会論文誌への投稿、カンファレンスや勉強会での登壇活動を通して、その脅威性の周知に努めてきました。</p>
<p>そして、こうした活動を続けてきた中で、大学院時代の担当教員である 上原哲太郎 先生から「Software Designで2038年問題について書いてみないか」とお声がけいただきました。上原先生との共著という形で、長年の思いが実を結び今回の執筆に至ったことを、大変嬉しく感じています。</p>
<h3 id="寄稿内容">寄稿内容</h3><p>記事では、UNIX timeが広く普及した歴史的経緯、それゆえの影響範囲の広さ、それによる2038年問題対応の困難さ、C言語における影響と64bit化対応といったトピックをご紹介しています。</p>
<p>組み込み開発に携わるエンジニアの方々はもちろん、ソフトウェア開発に関わるすべての方にとって、問題の理解と今後の対策の一助となれば幸いです。</p>
<p>ぜひ手に取ってご覧ください。</p>
<h2 id="業界貢献に対する思い">業界貢献に対する思い</h2><p>ここからは、私個人として業界貢献にどのような思いを抱いているのか、そして入社4ヶ月目の今、それをどのように育んでいきたいかをお話しできればと思います。</p>
<p>私は就職活動中、「会社に属してもソフトウェア業界全体と良い関係を築き、エンジニアリングが面白くあり続けてほしい」 という思いがありました。</p>
<p>フューチャーの多くの社員が執筆活動、OSS貢献、ガイドライン公開などに積極的に取り組んでいることを知り、私自身の目指す方向性と合致すると感じました。</p>
<p>私がフューチャーに入社して感じたのは、 <strong>社会全体のIT基盤を支えることが、ひいては自社の事業成長にも繋がるという考えから、多くの社員が業界貢献を重視している</strong> 傾向があるということです。</p>
<p>例えば、技術書や技術雑誌への執筆・翻訳活動を通じて、多くの社員がその知見を広く社会に発信しています。</p>
<p>今回の Software Designへの寄稿 もその一例ですし、過去には『Real World HTTP』や『実用 Go 言語』、『Webフロントエンド E2E テスト』といった書籍の執筆・翻訳にも携わっています。</p>
<p>また、社内で培った設計のノウハウをまとめたアーキテクチャ設計ガイドラインのように、自社の知見を外部へ公開し、広く役立ててもらう取り組みも行われています。</p>
<p>さらに、Vuls や Vue.jsといった、多くの開発者が利用するオープンソースプロジェクトへ積極的に貢献しているコントリビューターも数多くいます。</p>
<p><strong>私個人としても、日々の開発で利用させてもらっている多くのOSSや書籍といった共有資産に、自分も何か貢献できたらという思い</strong> があります。</p>
<p>会社がこうした活動を推奨し支援してくれることで、業界全体の成長とフューチャー自身の事業発展という好循環が生まれていると実感しています。</p>
<p>当面は、今回のような技術誌への寄稿や社内での知見共有を通じて、学んだことを着実に発信していきたいです。</p>
<p>そしてこれから経験を積んでいく中で、カンファレンスでの登壇やOSSプロジェクトへの貢献といったことにも、いずれは挑戦できるよう、一歩ずつ成長していければと考えています。</p>
<p>自身の専門性を高めながら、微力ながらも業界の発展に貢献できるエンジニアになれたら嬉しいです。</p>
<h2 id="2025年-8月号のみどころ個人的推し">2025年 8月号のみどころ個人的推し</h2><p>『Software Design 2025年8月号』は、私が寄稿させていただいた「2038年問題」の特別企画以外にも、個人的に心惹かれる記事が盛りだくさんの号です。</p>
<p>特に注目したいのは、リファクタリングの適切な判断とビジネス価値に焦点を当てた特集です。この特集には、なんとミノ駆動さんや及川卓也さんをはじめとする著名なエンジニアの方々も寄稿されており、現場で培われた実践的な知見がたくさん詰まっています。</p>
<p>「2038年問題を考える」コラムはもちろんのこと、他の素晴らしい記事にも目を通していただけると嬉しいです。</p>
<h2 id="おわりに">おわりに</h2><p>今回のSoftware Designへの寄稿は、入社4ヶ月目の私にとって非常に貴重な経験となりました。</p>
<p>学生時代からの「2038年問題」への取り組みが、このような形で世の中に発信する機会に繋がったこと、そして日頃から憧れていた執筆活動をスタートできたことを、心から嬉しく思っています。</p>
<p>この貴重な機会を与えてくださった技術評論社様には、深く感謝申し上げます。</p>
<p>今後もこの経験を糧に、精進してまいりますので、どうぞよろしくお願いいたします。</p>
]]></content>
    <summary type="html">Software Design 2025年8月号　特別企画「2038年問題を考える」に寄稿する機会をいただきました。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="SoftwareDesign" scheme="https://future-architect.github.io/tags/SoftwareDesign/"/>
    <category term="出版" scheme="https://future-architect.github.io/tags/%E5%87%BA%E7%89%88/"/>
  </entry>
  <entry>
    <title>子育てDIY：YouTubeの視聴をご褒美に変えた話</title>
    <link href="https://future-architect.github.io/articles/20250623a/"/>
    <id>https://future-architect.github.io/articles/20250623a/</id>
    <published>2025-06-22T15:00:00.000Z</published>
    <updated>2025-06-22T15:00:00.000Z</updated>
    <author><name>西田好孝</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>子育てをする中で、意外にも本職の知識でDIYして役に立つことってありませんか？子どもの年齢や家庭でのデバイス・IT環境も千差万別ありますが、抽象化した悩みとしては2つに集約すると思います。</p>
<ol>
<li>子どもが夢中になっている事を後押しするためにやる事</li>
<li>子どもが夢中になっている事を制限するためにやる事</li>
</ol>
<p>私の話で言えば、1はマインクラフトのサーバ構築やマルチプレイ環境の構築が当てはまり、<br>2は本記事の主題の通り、YouTube視聴に制限をかけてご褒美にした話です。</p>
<h2 id="具体的な悩み事">具体的な悩み事</h2><ul>
<li>男の子はとにかくゲーム&#x2F;YouTubeが大好き！放っておくと無限にやってしまいます！<ul>
<li>私の家庭では、やること自体はもちろんいいんですが、生活や体調に支障をきたすレベルに過度に依存しないルールを設けています</li>
<li>Nintendo Switchはみまもりアプリで時間の上限が設定できるのでそれで問題ありませんでした</li>
</ul>
</li>
<li>問題はテレビ&#x2F;DVD含めた映像媒体です。一度つけたら最後、そんなに見たいわけでもないのに宿題&#x2F;夕ご飯&#x2F;風呂&#x2F;歯磨きなど、とにかく自分で進められない。親が促しても『今じゃない』とかいろいろ言い訳つけて、21時過ぎてあわてて寝る準備するなんて言う日も普通にありました</li>
<li>そこで、『それらの準備を終わらせたらYouTubeを見ても良い』『早ければ早いほど見える時間が増える』、というルールと、それを実現するITインフラを準備しました</li>
</ul>
<h2 id="フローチャート">フローチャート</h2><ul>
<li>まず、子どもがYouTubeを見ることが出来るフローチャートはこんな感じです</li>
<li>基本的には、生活の上で最低限やるべきことをやった後で、ご褒美的な位置づけに変えました</li>
</ul>
<img fetchpriority="high" src="/images/2025/20250623a/flowchart.png" alt="flowchart.png" width="481" height="381">

<h2 id="構成">構成</h2><ul>
<li>構成に関しては、特に難しい要素はありません</li>
</ul>
<img src="/images/2025/20250623a/diagram.drawio.png" alt="diagram.drawio.png" width="591" height="341" loading="lazy">

<ul>
<li>Windows はいつの頃からか、pac ファイルはローカルを参照できなくなったので、github 上に pac を公開しています</li>
<li>後は Proxy コンテナを <code>docker compose up</code> するかどうかで Youtube を見えるかどうかを制御しています</li>
</ul>
<h2 id="Config-等">Config 等</h2><h3 id="子どものPC側の設定など">子どものPC側の設定など</h3><ul>
<li>子どものアカウント（標準ユーザ）にて、Pac ファイルの指定</li>
</ul>
<img src="/images/2025/20250623a/2025-06-12_222922.png" alt="2025-06-12_222922.png" width="1020" height="724" loading="lazy">

<ul>
<li><p>管理者アカウントにて、プロキシの設定を変更出来ない設定</p>
<ul>
<li><code>gpedit.msc</code> でグループポリシーエディタを起動</li>
</ul>
<img src="/images/2025/20250623a/2025-06-12_222625.png" alt="2025-06-12_222625.png" width="400" height="204" loading="lazy">

<ul>
<li><code>プロキシの変更が出来ない</code> というポリシーを有効にする</li>
</ul>
<img src="/images/2025/20250623a/2025-03-09_201437.png" alt="2025-03-09_201437.png" width="1200" height="704" loading="lazy"></li>
</ul>
<h3 id="github-に上がっているPac">github に上がっているPac</h3><ul>
<li>Pacファイル</li>
</ul>
<figure class="highlight js"><figcaption><span>user.pac</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">FindProxyForURL</span>(<span class="params">url, host</span>)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// . が含まれない場合。つまりローカルドメイン</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="title function_">isPlainHostName</span>(host))</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;DIRECT&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ローカルIP宛の通信は direct</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="title function_">isInNet</span>(host, <span class="string">&quot;192.168.0.0&quot;</span>, <span class="string">&quot;255.255.0.0&quot;</span>))</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;DIRECT&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ドメイン名に基づくproxyの設定 基本的には www.youtube.com を向ければ問題な</span></span><br><span class="line">い</span><br><span class="line">    <span class="keyword">if</span> (<span class="title function_">shExpMatch</span>(host, <span class="string">&quot;youtube.com&quot;</span>))</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;PROXY 192.168.11.64:10080&quot;</span>;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (<span class="title function_">shExpMatch</span>(host, <span class="string">&quot;*.youtube.com&quot;</span>))</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;PROXY 192.168.11.64:10080&quot;</span>;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (<span class="title function_">shExpMatch</span>(host, <span class="string">&quot;youtu.be&quot;</span>))</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;PROXY 192.168.11.64:10080&quot;</span>;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (<span class="title function_">shExpMatch</span>(host, <span class="string">&quot;youtubekids.com&quot;</span>))</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;PROXY 192.168.11.64:10080&quot;</span>;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (<span class="title function_">shExpMatch</span>(host, <span class="string">&quot;*.youtubekids.com&quot;</span>))</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;PROXY 192.168.11.64:10080&quot;</span>;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (<span class="title function_">shExpMatch</span>(host, <span class="string">&quot;*.netflix.com&quot;</span>))</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;PROXY 192.168.11.64:10080&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// どこにも合致しない場合は、諦めて direct 接続</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;DIRECT&quot;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="親のPC側でやること">親のPC側でやること</h3><ul>
<li>docker compose でプロキシを起動</li>
</ul>
<figure class="highlight yaml"><figcaption><span>compose.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">proxy:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">ubuntu/squid:latest</span></span><br><span class="line"><span class="comment">#    volumes: # マウントは特に要りませんでした、細かくホワイトリスト設定したい場合などは必要</span></span><br><span class="line"><span class="comment">#      - ./work:/work</span></span><br><span class="line"><span class="comment">#      - ./conf/squid.conf:/etc/squid/squid.conf</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;10080:3128&quot;</span></span><br></pre></td></tr></table></figure>

<ul>
<li>後は、子どもの準備が整ったら、<code>docker compose up</code> をすると <code>youtube.com</code> にアクセスが出来る様になります</li>
</ul>
<h2 id="その他細かい話">その他細かい話</h2><ul>
<li>YouTubeは、動画のストリーミング自体は、別の FQDN から行われているらしく、全量は調べられていません。なので動画を見ている途中でコンテナを落としても動画は見えてしまいます。でも、次の動画は見えない挙動になります</li>
<li>逆にこの仕様を逆手にとって、21:00にコンテナを落とすので、「その動画を最後まで見たら終わり」という整理にしています</li>
</ul>
<h2 id="あとがき">あとがき</h2><ul>
<li>話としては以上です。非常に簡単な仕組みなので、技術として難しい要素はないですね</li>
<li>子どもアカウント側で、『Proxy の変更が出来ない』制御はいらないかもしれません。『Proxy を変更すると見える！』という事がわかるくらいの年齢なら、どっちかというと自制心を育てる何かの活動をすべきです</li>
<li>タブレットやスマホも同様にwifi設定する際に、PAC URL を指定できるので、同じ制御が可能です<ul>
<li>可能ですが、SIMを入れて外で使いたい場合は、PACを外すか、PAC内で判別させるか、もう一歩複雑な処理を入れる必要があります</li>
</ul>
</li>
<li>Chrome Cast など経由ではなく、TVで直接 Youtube を見る場合は… ちょっとわかりません</li>
<li>Youtube のチャネルによって制御したいケースは、試していませんが、Proxyで中間CA局を立てて、中間サーバ証明書をhttpsリクエストのたびに発行する、という設定をすればおそらく細かい制御も出来るとは思いますが、httpsのどのリクエストパラメータにチャネル名が入っているのか？などは確認できていません</li>
<li>嫁の反応は、基本的には自分のスマホが取り上げられなくなる事、PCのキー操作を覚える事などは肯定的です。ですが、マインクラフトのマシンガントーク実況のチャンネルの音量がとても大きく、そこは否定的です😂</li>
</ul>
]]></content>
    <summary type="html">子育てをする中で、意外にも本職の知識でDIYして役に立つことってありませんか？子どもの年齢や家庭でのデバイス・IT環境も千差万別ありますが、抽象化した悩みとしては2つに集約すると思います</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="ネットワーク" scheme="https://future-architect.github.io/tags/Network/"/>
    <category term="プロキシ" scheme="https://future-architect.github.io/tags/%E3%83%97%E3%83%AD%E3%82%AD%E3%82%B7/"/>
    <category term="子ども" scheme="https://future-architect.github.io/tags/%E5%AD%90%E3%81%A9%E3%82%82/"/>
    <category term="育児" scheme="https://future-architect.github.io/tags/%E8%82%B2%E5%85%90/"/>
  </entry>
  <entry>
    <title>初めてのPostman</title>
    <link href="https://future-architect.github.io/articles/20250414a/"/>
    <id>https://future-architect.github.io/articles/20250414a/</id>
    <published>2025-04-13T15:00:00.000Z</published>
    <updated>2025-04-13T15:00:00.000Z</updated>
    <author><name>真野隼記</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250414a/Postman_(software).png" alt="" width="659" height="200">

<h2 id="はじめに">はじめに</h2><p>春の入門祭り2025 1日目です。</p>
<p>Web API開発プラットフォームとして高い人気を誇るPostmanの入門記事です。</p>
<h3 id="Postmanとは">Postmanとは</h3><p>PostmanはWeb API開発のあらゆる工程を支援するプラットフォームです。</p>
<p>この、Web API開発プラットフォームという用語は初見ではイメージがつきにくいかと思います。例えば、<code>curl</code> や VS Coode拡張の <code>REST Client</code> とどの程度違いがあるのか、 <code>OpenAPI Specification</code> などとの関連性は？などフワッとしてしまいます。</p>
<p>まず、競合？となりえそうな周辺のツールと比較すると輪郭が明確になるのではないかという仮説のもと作成したのが下表です。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">フェーズ&#x2F;機能</th>
<th align="left">詳細</th>
<th align="left">Postman</th>
<th align="left">競合ツールの例</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>設計</strong></td>
<td align="left">API定義 (OpenAPI) 作成・編集・検証</td>
<td align="left">✅ API Builder (OpenAPI v2, v3, v3.1対応)</td>
<td align="left">Swagger Editor, Stoplight Studio, VS Code (拡張機能)</td>
</tr>
<tr>
<td align="left"><strong>開発</strong></td>
<td align="left">モックサーバ</td>
<td align="left">✅ Collectionから生成可能。動的レスポンス対応。</td>
<td align="left">Prism, Mockoon, WireMock</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">クライアントコード生成</td>
<td align="left">✅ 主要言語向けの基本的なスニペット生成</td>
<td align="left">OpenAPI Generator</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">サーバスタブ生成</td>
<td align="left">❌</td>
<td align="left">OpenAPI Generator</td>
</tr>
<tr>
<td align="left"><strong>実行・デバッグ</strong></td>
<td align="left">GUIクライアント (Desktop&#x2F;Web)</td>
<td align="left">✅ リクエスト送信、環境変数、認証、スクリプト(JS)実行など多機能</td>
<td align="left">Insomnia, Thunder Client (VS Code拡張)</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">CUIクライアント</td>
<td align="left">✅ Postman CLI (Collection実行、CI連携), Newman</td>
<td align="left">curl, HTTPie</td>
</tr>
<tr>
<td align="left"><strong>テスト</strong></td>
<td align="left">機能テスト&#x2F;E2Eテスト</td>
<td align="left">✅ Collection Runner, スクリプトによるアサーション、データ駆動テスト、CI&#x2F;CD連携</td>
<td align="left">runn, Playwright (API Testing)</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">パフォーマンステスト (負荷テスト)</td>
<td align="left">✅ 基本的な負荷テスト (仮想ユーザー数制限あり ※プラン依存)</td>
<td align="left">k6, Gatling, JMeter</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">APIセキュリティテスト</td>
<td align="left">⚠️ 基本的なテストは可能だが、専門的な脆弱性スキャンは限定的</td>
<td align="left">OWASP ZAP, Burp Suite, 42Crunch</td>
</tr>
<tr>
<td align="left"><strong>運用・管理</strong></td>
<td align="left">APIゲートウェイ</td>
<td align="left">❌</td>
<td align="left">AWS API Gateway, Kong</td>
</tr>
<tr>
<td align="left"></td>
<td align="left">APIカタログ&#x2F;ディスカバリ</td>
<td align="left">✅ Private&#x2F;Public API Network (限定的)</td>
<td align="left">Backstage, Port</td>
</tr>
</tbody></table></div>
<p>これらの機能カバレッジを見ると、Postmanとしての主軸は「実行・デバック」を中心として、開発者体験を上げるように機能を追加していったのかなと推測できます（バイアスがかかった見方かもしれません）。</p>
<p>ただし、同じできることと言ってもPostmanは長年改善が続いているだけに、優れた仕組みになっているところも多いです。例えば、Web APIにリウクエスト送信ができるといっても、OAuth2認可の設定がPostmanでは容易に可能です。WebSocketや、少しレイヤが異なりますがGraphQLや最近MCPサーバでも注目度が高まっているJSON-RPCへの対応も、差別化要素です（Postmanはいずれも対応しています）。</p>
<h3 id="PostmanのWeb-APIクライアント機能について">PostmanのWeb APIクライアント機能について</h3><p>私が初めてPostmanを触ったのはおそらく2015年あたりで、当時はデスクトップアプリケーションしか存在しなかった印象が強いです。しかし、2025年の現在、多くの実行環境でPostmanを利用できます。リッチな <code>curl</code> 的な位置づけで利用を始めようとすると、大体以下の3つのうちどれかから選ぶことができると思います。この進化具合に最初、驚きました。</p>
<ul>
<li>ブラウザ</li>
<li>デスクトップアプリケーション</li>
<li>VS Code拡張</li>
</ul>
<p>それぞれの使い分けですが、基本的には以下のように選択、使い分けすると良いでしょう。</p>
<ul>
<li>ブラウザ<ul>
<li>デスクトップアプリケーショに比べ、一部、機能制約がある</li>
<li>とはいえ、初見はアプリのインストール無しで試せるので非常に気持が楽。使い勝手をまず試すにお勧め</li>
<li>制約というのは、主にCORSなどで生じ、機能を開放するためには別途、リクエストを仲介するローカルでリバースプロキシのように動作するミニアプリケーションをインストールが必要で、結局インストールするのであれば..という気持ちになりがち</li>
</ul>
</li>
<li>デスクトップアプリケーション<ul>
<li>機能が最も豊富で、制約も少ないため公式としても推奨している。インストールが可能であれば、本格的に利用する場合に利用すると良い</li>
</ul>
</li>
<li>VS Code拡張<ul>
<li>主要なプロトコルはカバーしているものの、GraphQLなどまだ利用できない機能がある</li>
<li>とはいえ、普段のエディタがVS Codeである場合、エディタの切り替えをしなくても済むという利点が大きく、Web開発が主体であれば適時取り入れておくと良さそう</li>
</ul>
</li>
</ul>
<h3 id="PostmanのWebアプリ版が個人的に面白い">PostmanのWebアプリ版が個人的に面白い!</h3><p>さて、ここで注目したいのが、PostmanのWebアプリ版です。WebAPIアクセスは <code>curl</code> で十分じゃないかなと思われる方も多いと思います。そんな中、なるべくツールのインストールなどのセットアップや依存関係なしに利用できるブラウザ版は、利点が大きいと思います。</p>
<p>しかし、気になるのはその実現手段です。通常、PostmanにログインしてWebアプリ版を利用すると、<code>postman.co</code> のサブドメインがユーザーごとに割り当てられます。そうすると、別ドメインのWeb APIサーバ（ほぼ全てがこのケースでしょう）に対してリクエストを送ると、通常は単純リクエストではないと思われるため（≒ <code>Content-Type:application/json</code> を設定すると思われるため）、ブラウザ側がプリフライトリクエストを送信してしまいます。</p>
<p>もちろん、Web APIサーバ側がこれに対して、 <code>postman.co</code> のサブドメインに対して、適切にOPTIONSメソッドで許可してもらえれば回避できますが、それだと利便性が著しく低いです。しかし、PostmanのWebアプリはこれを巧みに回避しています。初めて動かした際に、なんでこれが疎通できるんだろう？と驚きました。</p>
<img src="/images/2025/20250414a/postman.drawio.png" alt="postman.drawio.png" width="1200" height="401" loading="lazy">

<p>これを実現しているのが、「エージェント」という仕組みです。</p>
<h3 id="Webアプリ版-Postmanエージェント">Webアプリ版 Postmanエージェント</h3><p>Postmanエージェントは、Webアプリのフッターをクリックすると、選択肢が表示されます。デフォルトでは「自動選択」になっているため、初心者の人は意識する必要がまず無いでしょう。</p>
<img src="/images/2025/20250414a/エージェント選択.drawio.png" alt="エージェント選択.drawio.png" width="981" height="768" loading="lazy">

<p>おそらく、自動選択で「クラウドエージェント」となっている方が多いと思います。これの仕組みですが、直接PostmanのWeb画面から、指定のリクエストを送るのではなく、Postman社のサーバを経由してAPIがリクエストされる仕組みなようです。</p>
<img src="/images/2025/20250414a/postman-ページ3.drawio.png" alt="postman-ページ3.drawio.png" width="1200" height="515" loading="lazy">

<p>クラウドエージェントのサーバは <code>postman.co</code> のサブドメインと同一のエンドポイントであるため、CORSは発生しません。同時に、クラウドエージェントはブラウザではないため、本来のリクエスト先であるWeb APIサーバへのアクセスにCORSは発生しません。</p>
<p>この仕組みに気がつくと、「Cloud Agent Usage」の月毎の実行回数制限1000回の意味が分かります。</p>
<img src="/images/2025/20250414a/{199CD77C-F213-4B81-B894-07F3DCE668F7}.png" alt="{199CD77C-F213-4B81-B894-07F3DCE668F7}.png" width="514" height="425" loading="lazy">

<p>クラウドエージェントは、Postman側のサーバリソースを使用するため、少なくてもフリープランでは利用回数の上限があるという訳です。</p>
<p>しかし、簡易的に利用できるという意味ではクラウドエージェントの回数制限はほぼ気にする必要はなく、後述する回避方法もあるため中々考えられた仕組みに感じます。</p>
<h3 id="クラウドエージェントの制約">クラウドエージェントの制約</h3><p>クラウドエージェントは月1000回までという制約がありますが、他にもメジャーな制約があります。仕組み上、localhost やVPC内のプライベートなAPIへの接続はできません。</p>
<img src="/images/2025/20250414a/postman-ページ4-ページ4.drawio.png" alt="postman-ページ4-ページ4.drawio.png" width="851" height="831" loading="lazy">

<p>これは、クラウドエージェントが <code>postman.co</code> という外部環境に存在するため、原理的に回避しようがないでしょう（プライベートAPIの場合、クラウドエージェントが稼働する外部IPのインバウンドを開放することで、リクエストを到達させることも可能だと思いますが、セキュリティ上難しいことが多いでしょう）。</p>
<p>このような場合、Webアプリ版のPostmanではいくつかの対応方法があります。具体的にはエージェントを切り替えです。</p>
<ol>
<li>デスクトップエージェントを利用する</li>
<li>インターセプターエージェントを利用する</li>
<li>ブラウザエージェントを利用する</li>
</ol>
<h3 id="デスクトップエージェント">デスクトップエージェント</h3><p>デスクトップは先に述べたように、追加でインストールするミニアプリケーションです。リバースプロキシのように動作します。動作イメージとしては、下図のようにブラウザと同じPC上で動作し、WebAPIサーバへのリクエストを仲介します。</p>
<img src="/images/2025/20250414a/postman-ページ5.drawio.png" alt="postman-ページ5.drawio.png" width="1200" height="862" loading="lazy">

<p>概念的には上図のような構成ですが、内部的にはもう少し複雑な作りなようです。</p>
<figure><img src="/images/2025/20250414a/{1DF5F13E-7D7B-42A5-BFB9-0C0B573AA2C5}.png" alt="{1DF5F13E-7D7B-42A5-BFB9-0C0B573AA2C5}.png" width="1200" height="678" loading="lazy"><figcaption>※画像は https://blog.postman.com/introducing-the-postman-agent-send-api-requests-from-your-browser-without-limits/ より</figcaption></figure>
<p>上記のブログ記事にて、Postmanエージェントは、WebSocketを使用したTCP接続を利用とあり、またサーバサイドのAgent Serviceを経由するともあるため、デスクトップエージェントがブラウザからリクエスト情報を受信するフローはもう少し複雑かもしれません。</p>
<p>なお、このデスクトップエージェントは、Safariには対応していないため、Safariには後述するインターセプターエージェントが有効です。</p>
<h3 id="インターセプターエージェント">インターセプターエージェント</h3><p>次に紹介するのは、インターセプターエージェントという、「ブラウザの拡張機能」を用いた仕組みです。ブラウザの拡張機能でリクエストをフックできる仕組みが存在し、それを用いてブラウザの拡張機能上で、リクエストをプロキシすることで、CORSを回避しているようです。</p>
<p>拡張機能場だと、ブラウザで動作するスクリプトより高い実行権限があるということから可能になっているのだと思います。Safariやデスクトップエージェントをインストールできないが、Chrome拡張であれば可能（このケースがあるか不明ですが）に有効でしょう。</p>
<p>なお、インターセプターエージェントは、自己署名証明書が使えない、クライアント証明書の使用も不可など、いくつか制約も存在します。</p>
<h3 id="ブラウザエージェント">ブラウザエージェント</h3><p>最後に紹介するブラウザエージェントですが、これはブラウザから直接リクエストを送信するモデルです。そのため、CORSが発生すると利用できません。 <code>localhost</code> などの開発サーバなど限定された場合など、追加でインストール無しで使えるため便利かもしれません。</p>
<p>ちなみに、プリフライトリクエストが発生しなさそうなGET要求の場合には、けっこう使えるのでは？と思いましたが、サーバ側がOPTIONSメソッドを明示的に許可する動作をしない限り利用できませんでした。</p>
<p>例えば、GitHub Pagesを活用して擬似REST APIサーバーを作る Qiita にある、JSONファイルは GitHub Pagesでホスティングされています。ここにあるJSONファイルに対してリクエストを投げると、CORSエラーになります。リクエストヘッダなどを追加していないため、単純リクエストになるかと思いましたが、プリフライトリクエストが飛んでしまいました。</p>
<img src="/images/2025/20250414a/{94BE85A5-EDCC-44E6-B368-299B8912357F}.png" alt="{94BE85A5-EDCC-44E6-B368-299B8912357F}.png" width="1200" height="627" loading="lazy">

<p>なんでだろうとリクエストを確認すると、<code>Postman-Token</code> という項目がリクエストヘッダーに追加されていたためでした。</p>
<img src="/images/2025/20250414a/{9F76E88E-D220-4048-8BD1-B2CAAFF33A59}.png" alt="{9F76E88E-D220-4048-8BD1-B2CAAFF33A59}.png" width="1200" height="555" loading="lazy">

<p>これにより、単純リクエストではなくなり、プリフライトリクエストが発行。GitHub PagesはOPTIONSメソッドを返さないので、おそらく405 Method Not Allowedになり失敗。CORSエラーとなるという具合だと思います。</p>
<p>また、gRPC、GraphQLリクエストの送信には対応していないため、制約も多いです。</p>
<p>このあたり、各エージェントの制約については公式ドキュメントにまとめられているので、チェックすると良いでしょう。</p>
<p>https://learning.postman.com/docs/getting-started/basics/about-postman-agent/</p>
<h2 id="まとめ">まとめ</h2><p>PostmanのWeb版のエージェントの仕組みが色々考えられており、仕組みを理解しようとすると面白かったという話でした。</p>
<p>なお、最初からデスクトップ版をインストールできれば、例えばクラウドエージェントの1000回制約などもなくなりますし、あえてWeb版を使い続ける必要はないかもしれません。しかし、内部の構成を理解しておくと表面上の制約の理解度も高まり、トラブルシューティングにも強くなると思います。</p>
<p>それでは良いPostmanライフを。</p>
<p>最後まで読んでいただきありがとうございました。</p>
]]></content>
    <summary type="html">Web API開発プラットフォームとして高い人気を誇るPostmanの入門記事です。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="WebAPI" scheme="https://future-architect.github.io/tags/WebAPI/"/>
    <category term="便利ツール" scheme="https://future-architect.github.io/tags/%E4%BE%BF%E5%88%A9%E3%83%84%E3%83%BC%E3%83%AB/"/>
  </entry>
  <entry>
    <title>新卒 2 年目社員の Software Design 寄稿 </title>
    <link href="https://future-architect.github.io/articles/20241227a/"/>
    <id>https://future-architect.github.io/articles/20241227a/</id>
    <published>2024-12-26T15:00:00.000Z</published>
    <updated>2024-12-26T15:00:00.000Z</updated>
    <author><name>小澤泰河</name></author>
    <content type="html"><![CDATA[
<img fetchpriority="high" src="/images/2024/20241227a/TH320_642408.jpg" alt="" width="320" height="452">


<p>こんにちは。 Technology Innovation Group（TIG）所属の小澤です。2022 年の 10 月にフューチャーに新卒で入社しました。最近は、新規 Web アプリ構築の全体アーキテクチャ設計や開発をしています。</p>
<p>技術評論社様（以下敬称略）の <strong>『Software Design 2024 年 8 月号』</strong> に記事を寄稿する機会をいただけましたので、その話をご紹介します。</p>
<p>新卒 2 年目の社員が雑誌へ寄稿というと、どのような流れで進むのか想像がつかないという方も多いと思います。 この記事でその流れをご紹介し、興味のある方には、ぜひご自身でもチャレンジするきっかけにしていただければ幸いです。</p>
<h2 id="はじめに-Software-Design-とは">はじめに: Software Design とは</h2><p>技術評論社の『Software Design』は、IT エンジニア向けの月刊誌です。 その時々のトレンドに合わせて、入門から実践まで様々な特集が組まれます。</p>
<p>最近は 2018 年 - 2023 年の各号のデータを 1 冊の価格で購入できる『Software Design 総集編』も発売されました。 私自身も活用しており、まだ新卒からの経験年数が浅い身にとって、ここ数年の技術トレンドの変遷が一望できて面白いです。</p>
<h2 id="フューチャーの社員と執筆活動">フューチャーの社員と執筆活動</h2><p>さて、フューチャーの社員の一部は、通常の業務とは別に執筆していることがよくあります。 たとえば、</p>
<ul>
<li>『Real World HTTP』（オライリー・ジャパン）</li>
<li>『実用 Go 言語』（オライリー・ジャパン）</li>
<li>『ソフトウェア設計のトレードオフと誤り』（オライリー・ジャパン）</li>
<li>『［入門］Web フロントエンド E2E テスト』 （技術評論社）</li>
</ul>
<p>などの書籍（初版発行日順）は、いずれもフューチャー社員が執筆や翻訳に関わっています。</p>
<p>そして、書籍だけでなく雑誌『Software Design』に寄稿する例も多数あります。今年 2024 年は 8 月、9 月、10 月の 3 か月連続でフューチャー社員が寄稿しました。私が担当したのは 8 月号です。</p>
<p>9 月号と 10 月号については、ぜひ担当社員のブログ記事をご覧ください。</p>
<ul>
<li>Software Design 2024年9月号 Goのエラーハンドリングと向き合う ベストな設計戦略を徹底解剖を寄稿しました</li>
<li>Software Design 2024年10月号 受託開発における設計ドキュメントの課題と解決案 作成・管理のヒントを探るへの寄稿</li>
</ul>
<p>書籍や雑誌の執筆・寄稿に至る経緯は様々と聞いていますが、最近はこの技術ブログが端緒となるケースが多いように思います。 出版社の方がブログ記事を見てくださり、それを拡充・発展させる形で企画を進めるという流れです。</p>
<h2 id="Software-Design-8月号寄稿の経緯">Software Design 8月号寄稿の経緯</h2><h3 id="勉強会から技術ブログへ">勉強会から技術ブログへ</h3><p>私は 8 月号の 1 記事を担当させていただくことができましたが、そこに至った経緯もこの技術ブログに投稿した 2023 年の記事に始まります。</p>
<p>私は通常のプロジェクト業務とは別に、社内の「CCoE（Cloud Center of Excellence）」組織、すなわちクラウド活用を推進する組織の活動に参加しています。 当時その中での 1 つの取り組みとして、幅広いクラウド技術の学びを共有する勉強会が行われていました。 その勉強会で、私は近年注目を浴びている <strong>CDN エッジでのコード実行に関する入門的なまとめ</strong>を発表しました。</p>
<p>それ以前は、現代の Web アプリケーション開発で CDN がよく利用されることは把握していましたが、CDN のしくみや CDN エッジでコード実行する意義や特殊性、具体的な方法などはあまり分かっていませんでした。 そこを勉強し、自分の言葉で整理することで、できる範囲を増やしアーキテクチャ設計スキルを強化することが、この発表の目的の 1 つです。</p>
<p>この発表はマークダウン形式の資料にまとめていたので、そのまま技術ブログ記事にする提案をもらいます。そうして実際に投稿したのが「CDN 入門とエッジでのアプリケーション実行」です。</p>
<h3 id="Software-Design-の特集へ">Software Design の特集へ</h3><p>少し時間が経って 2024 年の前半、Software Design の編集者の方から、「技術ブログ記事を見たので、8 月号の特集を担当するのはどうか」という旨の大変貴重なご提案をいただきました。 8 月号のメイン特集は 2 本立てでその 1 つが全 3 章の「CDN エッジ」です。その第 1 章の入門記事を担当するご提案でした。ほかの章は別の担当の方が執筆します。</p>
<p>元の技術ブログ記事は、CDN の入門に始まり、 Web アプリと CDN の関係を復習し、Cloudflare Workers を例にエッジでのコード実行を実際に試して終わりという構成でしたが、8 月号特集ではハンズオンや実践例は後の章で行うということもあり、構成・目次を変更して内容を再整理することになります。</p>
<h3 id="記事の再構成">記事の再構成</h3><p>Software Design の読者は、完全初心者という人は多くないので、元の記事の初歩的な内容は最小限に絞り、各 CDN サービスの紹介や違い、具体的なユースケースを拡充しました。 編集者の方と何度かやり取りをして、より効果的な順序・内容となるようにブラッシュアップします。 最終的な構成は次のようになりました。</p>
<p>第1章 CDNエッジとWebアプリの関係性</p>
<ul>
<li><strong>速習 Web アプリ</strong> … 特集の論点を理解するために必要な Web アプリの前提知識をリクエストのマッピングに注目して整理します。</li>
<li><strong>Web アプリの実現形式</strong> … サーバーサイドレンダリング（SSR）、クライアントサイドレンダリング（CSR）を解説し、Web アプリの動的な振る舞いと静的な振る舞いを整理します。</li>
<li><strong>CDN と Web アプリ</strong> … SSR と CSR の合わせ技など、近年の Web フロンエンド事情を紹介し、各方式と CDN の関連性を見ます。</li>
<li><strong>CDN のしくみ</strong> … CDN の基本的な構成要素を整理します。</li>
<li><strong>ユーザーの近くのエッジロケーションに誘導するしくみ</strong> … CDN を実現する方式の一例として順を追って解説します。</li>
<li><strong>CDN サービスの例</strong> … CDN を提供するサービスの特徴を簡潔に紹介します。</li>
<li><strong>CDN エッジでのコード実行</strong> … エッジサーバーでコード実行ができるサービスの紹介と特徴、具体的なユースケースを解説します。</li>
<li><strong>CDN の使い道</strong> … 従来の用途から近年のエッジランタイムまで、CDN の役割を振り返り、どのような場面で CDN 関連の技術を活用できるか考えます。</li>
</ul>
<p>興味を持った箇所があれば、ぜひお読みいただければ嬉しいです。</p>
<h2 id="執筆のポイント">執筆のポイント</h2><p><strong>ここからは半分自分用の戒め</strong>なのですが、今後初めて書籍や雑誌記事を執筆するという方のためにも、個人的に感じた執筆のポイントを（良かった点と改善点を踏まえて）紹介します。</p>
<h3 id="大枠から決める・量を稼いで後で減らす">大枠から決める・量を稼いで後で減らす</h3><p>今回は元のブログ記事があったので、完全にゼロからのスタートではありません。 とはいえ、ブログ記事と雑誌特集では必要な内容も異なり、多くを新たに生み出す必要があります。</p>
<p><strong>何もない、あるいは混沌とした状態から「最初の形」を生み出すことは、かなりの負荷がかかるものです</strong>。 この負荷は、高難度な新しいソフトウェアの基盤部分を自分自身がリードして作らなければならないときの負荷に似ています。 気にすべき点や盛り込みたい点がたくさん思いつき、それを全部回収したものを作ろうとすると途方もない考慮が必要になってしまうものです。</p>
<p>そこで大事になるのは、<strong>大枠（目次）をまず雑に決めて、その後はとにかく量を稼ぐこと</strong>、<strong>いったん進捗率 100%（ただし精度 30%）という状態にすばやく到達すること</strong>だと感じます。</p>
<p>誤解のないように言っておくと、<strong>出版物の価値は量ではなく質で決まります</strong>。 しかし、<strong>あくまで執筆時のテクニックとしては</strong>、まず量を稼ぐようにしなければ質を向上させるだけの進捗を生めない側面があります。</p>
<p>いったんの形を作ることで、量を稼ぐ自己ノルマから解消され、質を上げるための余裕が生まれます。<br>雑に進めることは不安を感じるのですが、後で精度を上げたり変更したりするのは、意外と簡単にできるので大丈夫です。<br>また、編集者の方とのやりとりで軌道修正が生じる可能性は十分あり得るので、大枠だけでも早めに合わせておくのが大切だと感じます。</p>
<p>とはいえ、これは理想であって、最初のステップに負荷がかかることには変わりはないので、現実的には行き詰ってしまいがちです。私自身も今後うまくできるとは正直断言できません。<br>しかし、少なくとも意識として、まず量を稼ぐべきだと自分に言い聞かせることで、既存箇所の改善に目が行ってしまうといった問題を、ある程度は抑制できるでしょう。</p>
<h3 id="実務での利用経験が少ない場合も、個人で手を動かして試す">実務での利用経験が少ない場合も、個人で手を動かして試す</h3><p>入門や解説系の文章を書くとき、どうしても実務での利用経験がなかったり、少なかったりするものを扱う必要が出てきます。 私のように新卒からの年数が浅い人などは特にそうだと思います。</p>
<p>これは仕方のないことなのですが、他方で、試したことのないものを聞きかじりで紹介もできません。 当たり前の主張になってしまいますが、<strong>文章を書く「前」に、個人開発でよいので手を動かして試し、実践を作っておく</strong>ことが大切です。 文章を書いた「後」のチェックだけだと微妙なのは、自分の経験に基づいた「自分の言葉」で文章を書くことができなくなるからです。自分の言葉で書かないと、どこかの説明のつぎはぎや、意味理解をしない AI の説明のようになってしまいます。</p>
<h3 id="引用・参考元はメモしておく">引用・参考元はメモしておく</h3><p>基本的に自分の経験を通した自分の言葉で書くことは上で述べたことですが、他方で元の文章がある場合は、著作権法上の引用要件を満たすように、元の文章を改変せず明確に引用する必要があります。 引用の必要はない場合でも、参考元の情報を示すことが重要な場面は多いでしょう。</p>
<p>そうした<strong>引用・参考元は、後から辿るのが大変になるので、法的なリスクを避ける意味でも、調査時にしっかりメモしておく</strong>ことが大切です。</p>
<h3 id="文章を書く行為に慣れておく">文章を書く行為に慣れておく</h3><p>これは今回よかった点ですが、<strong>文章を書く行為に日々の業務で慣れておく</strong>ことはとても役に立ちました。 例えば、開発中のソフトウェアのドキュメントや説明資料を、ある程度まともな体裁で書いておくことです。 近年のモダンな OSS やサービスのドキュメントは、読み手を意識したわかりやすい書籍のような文体が増えていると感じます。 そのあたりを参考にして、<strong>業務のドキュメントをマークダウンや Web などの文章形式で書くことは良いトレーニング</strong>になりそうです。</p>
<h3 id="普段から各トピックの考えをまとめておく、論じ慣れておく">普段から各トピックの考えをまとめておく、論じ慣れておく</h3><p>先ほど「何もない、あるいは混沌とした状態から、最初の形を生み出すことは、かなりの負荷がかかる」と書きました。 しかし今後目指していきたいのは、<strong>そもそも「何もない、あるいは混沌とした状態」をスタート地点にするのではなく、「ある程度考えがまとまった状態」を普段から用意しておく</strong>、ということです。</p>
<p>たとえば、私はこれまで Web アプリのしくみを何度も説明する機会があり、かなり「論じ慣れ」ていました。そのため今回の記事執筆でも、慣れた箇所はほぼ負荷なく文章を書くことができました。 他方で、今回調査や実践から始めることとなった CDN エッジに関わる箇所は、負荷が高かったように思います。</p>
<p>このように、普段から考えが整理され、論じ慣れているトピックは、執筆負荷が低くなります。 出版物を執筆するのだから当然といえば当然なのですが、いざ書くときに困らないよう、理解と考えを表明するアウトプットを定期的に出しておくことは重要だと感じます。</p>
<p>ベテランの執筆陣は、このあたりのベースの力がやはり強いのだと思います。 私自身も（最近全然できていない）技術ブログへの投稿などを増やして鍛えていきたいところです。</p>
<h2 id="おわりに">おわりに</h2><p>この記事では、「新卒 2 年目社員の Software Design 寄稿」について、その経緯と振り返りを行いました。</p>
<p>将来の自分が次の本や雑誌に挑戦するとき、この記事を見直した上で、良いものを作れればと思います。<br>また、技術書や雑誌の執筆に興味がある方にとって、イメージがより鮮明になり、挑戦する気持ちを後押しできれば幸いです。</p>
]]></content>
    <summary type="html">技術評論社様（以下敬称略）の『Software Design 2024 年 8 月号』 に記事を寄稿する機会をいただけましたので、その話をご紹介します。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="CDN" scheme="https://future-architect.github.io/tags/CDN/"/>
    <category term="SoftwareDesign" scheme="https://future-architect.github.io/tags/SoftwareDesign/"/>
    <category term="Web" scheme="https://future-architect.github.io/tags/Web/"/>
    <category term="テクニカルライティング" scheme="https://future-architect.github.io/tags/%E3%83%86%E3%82%AF%E3%83%8B%E3%82%AB%E3%83%AB%E3%83%A9%E3%82%A4%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0/"/>
    <category term="出版" scheme="https://future-architect.github.io/tags/%E5%87%BA%E7%89%88/"/>
  </entry>
  <entry>
    <title>Cloudflare Snippetsをやってみた</title>
    <link href="https://future-architect.github.io/articles/20240614a/"/>
    <id>https://future-architect.github.io/articles/20240614a/</id>
    <published>2024-06-13T15:00:00.000Z</published>
    <updated>2024-06-13T15:00:00.000Z</updated>
    <author><name>亀田治伸</name></author>
    <content type="html"><![CDATA[<p>こんにちは。Cloudflareの亀田治伸です。</p>
<p>動画・音楽配信システム構築、決済代行事業者、AWSエバンジェリストを経て現職(Cloudflare)となります。得意領域は、認証、暗号、ネットワークを中心としたセキュリティ、 映像配信、開発手法に見る組織論、クラウドアーキテクチャ、プレゼンテーションなどです。</p>
<h2 id="はじめに">はじめに</h2><p>Cloudflare Snippetsは新しいCloudflareのサービスであり2023年6月にクローズドアルファ版がリリースされ、2024 Develoer Weekでは無作為に抽出された5％のユーザーが利用可能になっていました。6月頭より全ユーザーが利用可能となったサービスです。</p>
<p>https://blog.cloudflare.com/ja-jp/cloudflare-snippets-alpha-ja-jp</p>
<h2 id="Cloudflare-Snippets-とは">Cloudflare Snippets とは</h2><p>Cloudflare の CDN&#x2F;WAF はルールという機能を提供しています。変換ルール、キャッシュルール、オリジンルール、コンフィグルール、リダイレクトルール等多くの機能が存在しており簡単にHTTPベース通信の挙動を制御させることができます。</p>
<p>SnippetsはCloudflare Workersのプラットフォームを流用することで、ルールとして新しくJavaScriptの断片（これをSnippetsと呼びます）を実行させることができるようになりました。</p>
<img fetchpriority="high" src="/images/2024/20240614a/abstract.png" alt="" width="1200" height="325">

<h2 id="Cloudflare-Workers-との違い">Cloudflare Workers との違い</h2><p>Cloudflare Workers はWeb Workershttps://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers という技術を活用しており通信にJavaScriptの処理を割り込ませることができます。</p>
<p>ただし専用ドメインが割り当てられるためアプリケーション、もしくはDNSでCNAMEやAレコードを設定する必要がある等の変更伴います。一方SnippetsはそのままCDN&#x2F;WAFとして動作するのでより簡単に処理を割り込ませることができます。</p>
<p>また常に起動するWorkersと異なり、ルールエンジンと連携することで特定の条件に合致したときのみ起動、といった制御が可能です。</p>
<img src="/images/2024/20240614a/2.png" alt="" width="1200" height="766" loading="lazy">

<p>この性質により、1つ処理を1つのSnippetsとして分割しておくことで、1つの通信に複数のSnippetsを起動させる、ということも可能です。</p>
<p>また、Workersと異なり、最大実行時間は5msで最大メモリは2MB、パッケージの合計サイズは32KBと小さいのも特徴です。</p>
<h2 id="さっそくやってみる">さっそくやってみる</h2><p>ではやってみましょう。</p>
<ul>
<li>https://zenn.dev/kameoncloud/articles/6dec28de015f6f</li>
<li>https://future-architect.github.io/articles/20230427a/</li>
</ul>
<p>等を参考にCDN経由のサイトを立てます。</p>
<img src="/images/2024/20240614a/3.png" alt="" width="370" height="165" loading="lazy">

<p>マネジメントコンソールから<code>Rules</code>→<code>Snippets</code>をクリックします。<br><code>Create a Snippet</code>をクリックします。</p>
<img src="/images/2024/20240614a/4.png" alt="" width="1200" height="487" loading="lazy">

<p>デフォルトで提供されるサンプルSnippetをそのままデプロイするため<code>Configure to add Snippet rule</code>をクリックします。<br>次にSnippet機能を制御するルールを作成します。</p>
<img src="/images/2024/20240614a/5.png" alt="" width="1127" height="588" loading="lazy">

<p>こうすることでクライアントが日本からの通信のみSnippetを起動させるということができます。<br><code>Configure to create snippet</code>→<code>Save and deploy snippet</code>とボタンを押せば完了です。<br>サイトにアクセスすると以下のようにHeaderが付与されていれることがわかります。</p>
<img src="/images/2024/20240614a/6.png" alt="" width="874" height="264" loading="lazy">
デフォルトコードは以下です。

<figure class="highlight js"><figcaption><span>snippet.js</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// Enter Snippet code below</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">    <span class="keyword">async</span> <span class="title function_">fetch</span>(<span class="params">request</span>) &#123;</span><br><span class="line">        <span class="keyword">const</span> response = <span class="keyword">await</span> <span class="title function_">fetch</span>(request);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Clone the response so that it&#x27;s no longer immutable</span></span><br><span class="line">        <span class="keyword">const</span> newResponse = <span class="keyword">new</span> <span class="title class_">Response</span>(response.<span class="property">body</span>, response);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Add a custom header with a value</span></span><br><span class="line">        newResponse.<span class="property">headers</span>.<span class="title function_">append</span>(</span><br><span class="line">            <span class="string">&quot;x-snippets-hello&quot;</span>,</span><br><span class="line">            <span class="string">&quot;Hello from Cloudflare Snippets&quot;</span></span><br><span class="line">        );</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Delete headers</span></span><br><span class="line">        newResponse.<span class="property">headers</span>.<span class="title function_">delete</span>(<span class="string">&quot;x-header-to-delete&quot;</span>);</span><br><span class="line">        newResponse.<span class="property">headers</span>.<span class="title function_">delete</span>(<span class="string">&quot;x-header2-to-delete&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Adjust the value for an existing header</span></span><br><span class="line">        newResponse.<span class="property">headers</span>.<span class="title function_">set</span>(<span class="string">&quot;x-header-to-change&quot;</span>, <span class="string">&quot;NewValue&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span> newResponse;</span><br><span class="line">    &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>

<h2 id="まとめ">まとめ</h2><p>いかがでしたでしょうか。</p>
<p>Cloudflareは他にもHTTPレイヤーを中心として様々な通信を処理する機能を提供しています。</p>
<p>https://zenn.dev/kameoncloud</p>
<p>でその機能を試してまとめているので興味のある方はぜひご覧ください。</p>
]]></content>
    <summary type="html">Cloudflare Snippetsは新しいCloudflareのサービスであり2023年6月にクローズドアルファ版がリリースされ、2024 Develoer Weekでは無作為に抽出された5%のユーザーが利用可能になっていました。6月頭より全ユーザーが利用可能となったサービスです。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="Cloudflare" scheme="https://future-architect.github.io/tags/Cloudflare/"/>
    <category term="外部寄稿" scheme="https://future-architect.github.io/tags/%E5%A4%96%E9%83%A8%E5%AF%84%E7%A8%BF/"/>
  </entry>
  <entry>
    <title>Cloudflare R2 + NextCloudで作る自分専用クラウドストレージのススメ</title>
    <link href="https://future-architect.github.io/articles/20240603a/"/>
    <id>https://future-architect.github.io/articles/20240603a/</id>
    <published>2024-06-02T15:00:00.000Z</published>
    <updated>2024-06-02T15:00:00.000Z</updated>
    <author><name>大岩潤矢</name></author>
    <content type="html"><![CDATA[<p>Cloudflare連載の4つ目です。</p>
<h2 id="はじめに">はじめに</h2><p>みなさんこんにちは、TIG所属の大岩潤矢( @920OJ )です。</p>
<p>Cloudflareのサービス、Cloudflare R2と、NextCloudを利用した自分専用クラウドストレージの構築について紹介します。</p>
<h2 id="Cloudflare-R2とは">Cloudflare R2とは</h2><p>Cloudflare R2とは、Cloudflareが提供するオブジェクトストレージサービスです。誤解を恐れず簡単に言ってしまえば、Amazon S3のCloudflare版、という表現が当てはまると思います。</p>
<p>https://www.cloudflare.com/ja-jp/developer-platform/r2/</p>
<h3 id="料金体系">料金体系</h3><p>Cloudflare R2の大きな特徴は、データ転送量が無料であることから、<strong>他社サービスに比べてコストを抑えて利用できること</strong>が挙げられます。以下にCloudflare R2の料金体系を整理します。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>種別</th>
<th>月間無料枠</th>
<th>有料(月額)</th>
</tr>
</thead>
<tbody><tr>
<td>ストレージ(データの保存)</td>
<td>10GB</td>
<td>1GB あたり 0.015 USD</td>
</tr>
<tr>
<td>クラスAの操作(状態の変更)</td>
<td>100万操作</td>
<td>100万操作 あたり 4.50 USD</td>
</tr>
<tr>
<td>クラスBの操作(既存状態の読取)</td>
<td>1,000万操作</td>
<td>100万操作 あたり 0.36 USD</td>
</tr>
<tr>
<td>転送(エグレス)</td>
<td>無制限</td>
<td>-</td>
</tr>
</tbody></table></div>
<p>詳細はCloudflare R2の公式ガイドもご確認ください。</p>
<p>https://developers.cloudflare.com/r2/pricing/</p>
<p>例えば保存しているデータが10GB以内である場合、ずっと無料のまま使い続ける事ができます。転送量に課金は発生しないので、どれだけ転送しても（1,000万回以上操作しない限りは）無料です。</p>
<p>一方ストレージの課金は「ピークストレージの平均」という概念で計算されるため、注意が必要です。これは日割りで保存しているデータ量を計算するもので、月の最終日の保存量に対する課金ではありません。</p>
<blockquote>
<p>A GB-month is calculated by averaging the peak storage per day over a billing period (30 days)</p>
</blockquote>
<p>例えばある月の1日〜15日までは100GBを保存していたが、データを削除して16日〜30日は1GBまで減らした場合、以下の計算式で課金料が決定します。</p>
<blockquote>
<p>100GB * 15&#x2F;30 month + 1GB * 15&#x2F;30 month &#x3D; 50.5 GB-month<br>(50.5GB-month - 10GB 無料枠) * 0.015 USD &#x3D; 0.6075 USD</p>
</blockquote>
<p>Amazon S3とも比較してみましょう。微妙に料金体系は異なりますが、先程の表に当てはめてみると以下のようになります。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>種別</th>
<th>月間無料枠</th>
<th>有料(月額)</th>
</tr>
</thead>
<tbody><tr>
<td>ストレージ(最初の50TB)</td>
<td>5GB</td>
<td>1GB あたり 0.025 USD</td>
</tr>
<tr>
<td>PUT, COPY, POST, LIST操作</td>
<td>2,000操作</td>
<td>100万操作 あたり 4.7 USD</td>
</tr>
<tr>
<td>GET, SELECT操作</td>
<td>20,000操作</td>
<td>100万操作 あたり 0.37 USD</td>
</tr>
<tr>
<td>転送(最初の1TB)</td>
<td>100GB</td>
<td>1GBあたり0.114 USD</td>
</tr>
</tbody></table></div>
<p>先ほど計算した同様のケースをAmazon S3で計算してみると以下のようになります。約2倍ほど高いようです。</p>
<blockquote>
<p>100GB * 15&#x2F;30 month + 1GB * 15&#x2F;30 month &#x3D; 50.5 GB-month<br>(50.5GB-month - 5GB 無料枠) * 0.025 USD &#x3D; 1.1375 USD</p>
</blockquote>
<h2 id="NextCloudとは">NextCloudとは</h2><p>オープンソースで開発されているNextCloudというセルフホスト型のストレージソフトウェアを利用し、自分専用のクラウドストレージを作成します。</p>
<p>https://nextcloud.com/</p>
<p>かつてownCloudというソフトウェアからフォークされたもので、ownCloudと比べるとチャットやビデオ会議機能が追加されているようです。</p>
<p>NextCloudでは基本的にNextCloudがインストールされたマシンのストレージをデータの保存場所としますが、「External Storage Support」というアプリ(プラグイン)をインストールすることで、各種外部ストレージサービスを保存場所に設定できます。</p>
<p>対応している外部ストレージサービスは以下のとおりです。</p>
<ul>
<li>Amazon S3</li>
<li>FTP</li>
<li>NextCloud</li>
<li>OpenStack Object Storage</li>
<li>SFTP</li>
<li>WebDAV</li>
<li>ローカル</li>
</ul>
<h3 id="なぜCloudflare-R2とNextcloudを組み合わせると良いのか？">なぜCloudflare R2とNextcloudを組み合わせると良いのか？</h3><p>今回は、NextCloudからCloudflare R2へ接続することにより、容量無制限かつ低コストの自分専用オンラインクラウドストレージを実現してみようと思います。</p>
<p>先述の通り、NextCloudでは外部ストレージとしてAmaon S3が利用できます。さらに、Cloudflare R2はAmazon S3のAPIと互換性があるため、NextCloudのバックエンドとしてCloudflare R2が利用できるという算段です。</p>
<p>この構成を使うのではなく、一般的なクラウドストレージサービス(Google Drive, iCloud等)で良いのでは？ という声が聞こえてくるかもしれません。しかし、これには無視できない絶妙なコスト差が発生します。</p>
<p>以下は一般的なクラウドストレージサービスの料金比較です。</p>
<div class="scroll"><table>
<thead>
<tr>
<th></th>
<th>Google Drive</th>
<th>OneDrive</th>
<th>iCloud</th>
<th>Dropbox</th>
</tr>
</thead>
<tbody><tr>
<td>無料枠</td>
<td>15GB</td>
<td>5GB</td>
<td>5GB</td>
<td>2GB</td>
</tr>
<tr>
<td>50GB</td>
<td>なし</td>
<td>なし</td>
<td>130円&#x2F;月</td>
<td>なし</td>
</tr>
<tr>
<td>100GB</td>
<td>250円&#x2F;月</td>
<td>203.3円&#x2F;月</td>
<td>なし</td>
<td>なし</td>
</tr>
<tr>
<td>200GB</td>
<td>380円&#x2F;月</td>
<td>なし</td>
<td>400円&#x2F;月</td>
<td>なし</td>
</tr>
<tr>
<td>1TB</td>
<td>なし</td>
<td>1,241.7円&#x2F;月</td>
<td>なし</td>
<td>なし</td>
</tr>
<tr>
<td>2TB</td>
<td>1,300円&#x2F;月</td>
<td>なし</td>
<td>1,300円&#x2F;月</td>
<td>1,500円</td>
</tr>
</tbody></table></div>
<p>一般的なクラウドストレージサービスでは、「使った分だけ課金する」従量課金ではなく、あらかじめ使いたい分のストレージ代金を毎月払っていくことになります。そして、往々にして各社用意しているプランは200GBから2TBまでの間が飛んでおり、例えば201GBのファイルを保存したい場合にでも2TB分の料金を払わざるを得ません。</p>
<p>ここで嬉しいのがCloudflare R2です。Cloudflare R2では従量課金であり、201GB保存した場合でも201GB分の料金を払うだけで済みます。計算すると2.865 USDで、1USD &#x3D; 160円換算だと458.4円です。<strong>Google Driveでは1,300円払わなければならないところが、この方法だと458円で収まりました。</strong></p>
<p>ちなみにCloudflare R2でのストレージ課金額が1,300円を超えるのはCloudflareに合計551.67GB以上のファイルを保存した場合です。(1USD &#x3D; 160円換算の場合なので、円高になればより多くのファイルを保存できます)</p>
<p>つまり、<strong>保存したいファイルの総量が551.67GBに達する前まではこの方法のほうがおトク</strong>といえるでしょう。(2024年6月現在)</p>
<p>また、2TBを超える場合など既存のクラウドストレージの提供するプランを超えてしまっているときなども良い選択肢になるでしょう。一方で、同様のサービスにWasabiというオブジェクトストレージサービスがあります。こちらはCloudflare R2よりも安い1TB6.99USDで運用できるので、比較検討してみるのもよいでしょう。</p>
<p>https://wasabi.com/pricing</p>
<h2 id="セットアップ">セットアップ</h2><p>ここからは、実際にCloudflare R2とNextCloudを利用して自分専用のクラウドストレージを構築する方法について、ハンズオンの形で紹介します。</p>
<p>今回はプロジェクトの後輩にRaspberry Piを布教する目的も兼ねて、手持ちのRaspberry Pi 3 Model BにNextCloudをインストールします。大学生時代に4台まとめ買いして家に転がっていたものです。</p>
<img fetchpriority="high" src="/images/2024/20240603a/image.png" alt="image.png" width="677" height="353">

<p>事前にIPアドレスを固定し、SSH接続できるようにしてあります。この方法は本題からそれてしまうので、割愛します。</p>
<p>（もしセットアップにDockerが使えるのであれば、Dockerを使ったほうが何倍も楽にスタートできるのでおすすめです。今回のようにDockerを動かすにはスペック不足であったり、何らかの原因によってインストール出来ない場合にのみ、以下の方法で実施しましょう）。</p>
<h3 id="MariaDB・nginx・PHP環境のインストール">MariaDB・nginx・PHP環境のインストール</h3><p>インストールを始める前に、パッケージを最新化しておきます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt upgrade</span><br></pre></td></tr></table></figure>

<p>データベースにはMariaDBを利用します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install mariadb-server</span><br></pre></td></tr></table></figure>

<p><code>mysql_secure_installation</code> コマンドでMariaDBのセットアップを実施します。自分は以下のようにセットしました。</p>
<ul>
<li>Switch to unix_socket authentication → No</li>
<li>Change the root password? → Yes</li>
<li>Remove anonymous users? → Yes</li>
<li>Disallow root login remotely? → No</li>
<li>Remove test database and access to it? → Yes</li>
<li>Reload privilege tables now? → Yes</li>
</ul>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> mysql_secure_installation</span></span><br><span class="line"></span><br><span class="line">NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB</span><br><span class="line">      SERVERS IN PRODUCTION USE!  PLEASE READ EACH STEP CAREFULLY!</span><br><span class="line"></span><br><span class="line">In order to log into MariaDB to secure it, we&#x27;ll need the current</span><br><span class="line">password for the root user. If you&#x27;ve just installed MariaDB, and</span><br><span class="line">haven&#x27;t set the root password yet, you should just press enter here.</span><br><span class="line"></span><br><span class="line">Enter current password for root (enter for none):</span><br><span class="line">OK, successfully used password, moving on...</span><br><span class="line"></span><br><span class="line">Setting the root password or using the unix_socket ensures that nobody</span><br><span class="line">can log into the MariaDB root user without the proper authorisation.</span><br><span class="line"></span><br><span class="line">You already have your root account protected, so you can safely answer &#x27;n&#x27;.</span><br><span class="line"></span><br><span class="line">Switch to unix_socket authentication [Y/n] n</span><br><span class="line"> ... skipping.</span><br><span class="line"></span><br><span class="line">You already have your root account protected, so you can safely answer &#x27;n&#x27;.</span><br><span class="line"></span><br><span class="line">Change the root password? [Y/n] y</span><br><span class="line">New password:</span><br><span class="line">Re-enter new password:</span><br><span class="line">Password updated successfully!</span><br><span class="line">Reloading privilege tables..</span><br><span class="line"> ... Success!</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">By default, a MariaDB installation has an anonymous user, allowing anyone</span><br><span class="line">to log into MariaDB without having to have a user account created for</span><br><span class="line">them.  This is intended only for testing, and to make the installation</span><br><span class="line">go a bit smoother.  You should remove them before moving into a</span><br><span class="line">production environment.</span><br><span class="line"></span><br><span class="line">Remove anonymous users? [Y/n] y</span><br><span class="line"> ... Success!</span><br><span class="line"></span><br><span class="line">Normally, root should only be allowed to connect from &#x27;localhost&#x27;.  This</span><br><span class="line">ensures that someone cannot guess at the root password from the network.</span><br><span class="line"></span><br><span class="line">Disallow root login remotely? [Y/n] n</span><br><span class="line"> ... skipping.</span><br><span class="line"></span><br><span class="line">By default, MariaDB comes with a database named &#x27;test&#x27; that anyone can</span><br><span class="line">access.  This is also intended only for testing, and should be removed</span><br><span class="line">before moving into a production environment.</span><br><span class="line"></span><br><span class="line">Remove test database and access to it? [Y/n] y</span><br><span class="line"> - Dropping test database...</span><br><span class="line"> ... Success!</span><br><span class="line"> - Removing privileges on test database...</span><br><span class="line"> ... Success!</span><br><span class="line"></span><br><span class="line">Reloading the privilege tables will ensure that all changes made so far</span><br><span class="line">will take effect immediately.</span><br><span class="line"></span><br><span class="line">Reload privilege tables now? [Y/n] y</span><br><span class="line"> ... Success!</span><br><span class="line"></span><br><span class="line">Cleaning up...</span><br><span class="line"></span><br><span class="line">All done!  If you&#x27;ve completed all of the above steps, your MariaDB</span><br><span class="line">installation should now be secure.</span><br><span class="line"></span><br><span class="line">Thanks for using MariaDB!</span><br></pre></td></tr></table></figure>

<p><code>mysql -u root -p</code> でログインできるか確かめておきましょう。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">mysql -u root -p</span></span><br><span class="line">Enter password:</span><br><span class="line">Welcome to the MariaDB monitor.  Commands end with ; or \g.</span><br><span class="line">Your MariaDB connection id is 38</span><br><span class="line">Server version: 10.11.6-MariaDB-0+deb12u1 Debian 12</span><br><span class="line"></span><br><span class="line">Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.</span><br><span class="line"></span><br><span class="line">Type &#x27;help;&#x27; or &#x27;\h&#x27; for help. Type &#x27;\c&#x27; to clear the current input statement.</span><br><span class="line"></span><br><span class="line">MariaDB [(none)]&gt;</span><br></pre></td></tr></table></figure>

<p>ログインできたら、以下のSQLを実行してNextCloud用のデータベースとユーザを作成しておきましょう。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> DATABASE nextcloud;</span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> nextcloud IDENTIFIED <span class="keyword">BY</span> <span class="string">&#x27;任意のパスワード&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">ALL</span> <span class="keyword">ON</span> nextcloud.<span class="operator">*</span> <span class="keyword">TO</span> nextcloud;</span><br></pre></td></tr></table></figure>

<p>Webサーバはnginxをインストールします。Apacheでも良いですが、メモリがカツカツなので、個人的にはnginxをおすすめします。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install nginx</span><br></pre></td></tr></table></figure>

<p>インストールできたら、systemctlコマンドで自動起動を有効化しておきます。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> nginx</span><br></pre></td></tr></table></figure>

<p>ブラウザからRaspberry piのIPアドレス宛にアクセスし、nginxのデフォルトページが表示されたらOKです。</p>
<img src="/images/2024/20240603a/image_2.png" alt="image.png" width="913" height="574" loading="lazy">

<p>続いてPHPをインストールします。デフォルトではPHP 8.2がインストールされるようだったので、リポジトリを追加してPHP 8.3をインストールします。以下コマンドを実行し、 <code>php8.3</code> と <code>php8.3-fpm</code> 、それから各種モジュールをインストールします。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> wget -qO /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;deb https://packages.sury.org/php/ <span class="subst">$(lsb_release -sc)</span> main&quot;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/apt/sources.list.d/php.list</span><br><span class="line"><span class="built_in">sudo</span> apt update</span><br><span class="line"><span class="built_in">sudo</span> apt install php8.3 php8.3-fpm php-mysql php-gd php-common php-xml php-json php-intl php-pear php-imagick php-dev php-mbstring php-zip php-soap php-bz2 php-bcmath php-gmp php-apcu php-curl</span><br></pre></td></tr></table></figure>

<p><code>php8.3-fpm</code> も同様にsystemctlコマンドで自動起動するようにしましょう。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> php8.3-fpm</span><br></pre></td></tr></table></figure>

<p><code>/etc/nginx/sites-enabled/default</code> を編集し、PHPを有効化しておきます。また、NextCloudに必要なリライトルール等もここに記載します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">location ~ \.php(?:$|/) &#123;</span><br><span class="line">        include snippets/fastcgi-php.conf;</span><br><span class="line">        rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php<span class="variable">$request_uri</span>;</span><br><span class="line"></span><br><span class="line">        fastcgi_split_path_info ^(.+?\.php)(/.*)$;</span><br><span class="line">        <span class="built_in">set</span> <span class="variable">$path_info</span> <span class="variable">$fastcgi_path_info</span>;</span><br><span class="line"></span><br><span class="line">        fastcgi_pass unix:/run/php/php8.3-fpm.sock;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>今回は必要最低限のリライトルールを記載しましたが、本来は以下の公式ドキュメントにあるように、色々設定する必要がありそうです。本番運用する場合は設定しておきましょう。</p>
<p>https://docs.nextcloud.com/server/latest/admin_manual/installation/nginx.html</p>
<p>ついでにPHPの設定を変えておきましょう。自分の場合セットアップがタイムアウトで落ちるようでしたので、タイムアウト時間を30秒から10分に伸ばしておきます。<code>/etc/php/8.3/php-fpm/php.ini</code> を編集します。</p>
<figure class="highlight ini"><figcaption><span>php.ini</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">max_execution_time</span> = <span class="number">600</span></span><br></pre></td></tr></table></figure>

<h3 id="NextCloudのインストール">NextCloudのインストール</h3><p>続いてNextCloudのインストールを実施します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">chown</span> -R www-data:www-data /var/www/html</span><br><span class="line"><span class="built_in">cd</span> /var/www/html</span><br><span class="line">wget https://download.nextcloud.com/server/installer/setup-nextcloud.php</span><br></pre></td></tr></table></figure>

<p>ファイルを配置したら、<code>http://(Raspberry PiのIPアドレス)/setup-nextcloud.php</code> にアクセスします。PHPが正しくセットアップされていればSetup Wizardが表示されるので、「Next」を押下します。</p>
<img src="/images/2024/20240603a/image_3.png" alt="image.png" width="416" height="489" loading="lazy">

<p>今回は <code>/var/www/html</code> 配下にそのままインストールするため <code>.</code> を入力して「Next」を押下。</p>
<img src="/images/2024/20240603a/image_4.png" alt="image.png" width="436" height="468" loading="lazy">

<p>インストールが終了したら、「Next」を押下します。</p>
<img src="/images/2024/20240603a/image_5.png" alt="image.png" width="592" height="351" loading="lazy">

<p>続いて、 <code>http://(Raspberry PiのIPアドレス)/index.php</code> にアクセスし、管理者情報を入力します。</p>
<p>データベース情報の部分は、先述したSQLを実行した場合は、データベース名・ユーザ名ともに <code>nextcloud</code> になります。</p>
<img src="/images/2024/20240603a/image_6.png" alt="image.png" width="1200" height="652" loading="lazy">

<p>ようやくインストール完了です。</p>
<img src="/images/2024/20240603a/image_7.png" alt="image.png" width="1200" height="644" loading="lazy">

<h3 id="Cloudflare-R2バケットの作成">Cloudflare R2バケットの作成</h3><p>いよいよ本題です。Cloudflareの管理画面にアクセスし、R2バケットを作成します。</p>
<p>管理画面へログインし、左のメニューよりR2へ進み、「Create bucket」ボタンを押下します。</p>
<img src="/images/2024/20240603a/image_8.png" alt="image.png" width="1082" height="583" loading="lazy">

<p>任意の名前を入力し「Create bucket」を押下します。</p>
<img src="/images/2024/20240603a/image_9.png" alt="image.png" width="897" height="662" loading="lazy">

<p>続いてAPIトークンを発行します。R2メニューの右上「Manage R2 API Tokens」に進みます（ユーザプロフィールから発行できるユーザトークンとはまた別なので注意）</p>
<img src="/images/2024/20240603a/image_10.png" alt="image.png" width="1200" height="380" loading="lazy">

<p>「Create API Token」を押下します。</p>
<img src="/images/2024/20240603a/image_11.png" alt="image.png" width="1192" height="328" loading="lazy">

<p>今回は閲覧だけでなく編集も行うため、最上位権限である「Admin Read &amp; Write」を選択し、新規作成します。</p>
<img src="/images/2024/20240603a/image_12.png" alt="image.png" width="1200" height="628" loading="lazy">

<p>最後にキーが払い出されます。以下の値をコピーしておきましょう。</p>
<ul>
<li>Token</li>
<li>Access Key ID</li>
<li>Secret Access Key</li>
<li>Endpoint</li>
</ul>
<img src="/images/2024/20240603a/image_13.png" alt="image.png" width="1200" height="728" loading="lazy">

<h3 id="接続設定">接続設定</h3><p>再度NextCloud側に戻り、接続設定を行います。</p>
<p>先述の通り、デフォルトでは外部ストレージ機能は有効化されていません。S3(Cloudflare R2)へ接続するため、Appsから有効化する必要があります。</p>
<p>右上のユーザアイコン→アプリ→注目のアプリへ進み、「External storage support」を有効化します。</p>
<img src="/images/2024/20240603a/image_14.png" alt="image.png" width="1179" height="415" loading="lazy">

<p>その後設定画面に進み、「外部ストレージ」メニューからR2の設定を追加します。先ほど控えたR2のアクセスキー等を以下のように入力します。</p>
<img src="/images/2024/20240603a/image_15.png" alt="image.png" width="1200" height="563" loading="lazy">

<p>最後にチェックマークをクリックし、左側に緑マークが点灯すれば設定完了です！</p>
<h2 id="使ってみる">使ってみる</h2><p>ホーム画面に戻ってみると、先ほど設定した「R2」フォルダが増えています。</p>
<img src="/images/2024/20240603a/image_16.png" alt="image.png" width="1134" height="504" loading="lazy">

<p>実際にファイルをアップロードしてみましょう。ファイルをドラッグ&amp;ドロップすると……</p>
<img src="/images/2024/20240603a/image_17.png" alt="image.png" width="1057" height="486" loading="lazy">

<p>無事アップロードできました！</p>
<p>Cloudflare R2側にも反映されているようです。</p>
<img src="/images/2024/20240603a/image_18.png" alt="image.png" width="1200" height="537" loading="lazy">

<h2 id="おわりに">おわりに</h2><p>NextCloudのバックエンドとしてCloudflare R2を用いることにより、ある条件下ではおトクに自分専用のクラウドストレージを構築できました。ぜひ自宅に使っていないラズパイやミニPCが転がっている方、既存クラウドストレージサービスの見直しをしている方は、ぜひ構築してみてください！</p>
<p>以上、Cloudflare R2 + NextCloudで作る自分専用クラウドストレージのススメでした。</p>
]]></content>
    <summary type="html">今回はCloudflare連載ということで、Cloudflareのサービス、Cloudflare R2と、NextCloudを利用した自分専用クラウドストレージの構築について紹介します。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="Cloudflare" scheme="https://future-architect.github.io/tags/Cloudflare/"/>
    <category term="S3" scheme="https://future-architect.github.io/tags/S3/"/>
  </entry>
  <entry>
    <title>CloudflareでWebサイトのメンテナンスイン/アウトを実装</title>
    <link href="https://future-architect.github.io/articles/20240531a/"/>
    <id>https://future-architect.github.io/articles/20240531a/</id>
    <published>2024-05-30T15:00:00.000Z</published>
    <updated>2024-05-30T15:00:00.000Z</updated>
    <author><name>小林弘樹</name></author>
    <content type="html"><![CDATA[<p>Cloudflare連載4日目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>TIG DXチームの小林弘樹です。</p>
<p>近年、CloudflareをCDNやDNSで活用しているシステムが増えている印象があります。宮崎さんの記事でもCloudflareを採用した事例が紹介されています。</p>
<p>CloudflareをCDNやDNSに利用しているサービスにおいて、CDNレイヤでメンテナンスイン&#x2F;アウトを実装する方法を書いてみます。</p>
<h2 id="概要">概要</h2><h3 id="やりたいこと">やりたいこと</h3><ul>
<li>Cloudflare上にメンテナンスページ（htmlファイル）をデプロイする</li>
<li>運用保守拠点からのアクセスはオリジンにアクセスさせ、その他一般ユーザーからのアクセスはメンテナンスページにリダイレクトさせる</li>
<li>Cloudflare APIを利用して、メンテナンスイン&#x2F;アウトを自動化する</li>
</ul>
<h3 id="利用サービス">利用サービス</h3><ul>
<li>Cloudflare R2</li>
<li>Cloudflare Rules(Single Redirects)</li>
</ul>
<h2 id="ドメイン取得・管理">ドメイン取得・管理</h2><p>今回の構成はCloudflare上でドメインを管理していることが前提となります。<br>Cloudflareでドメインを取得したり、取得済みのドメインをCloudflareに移管する方法については、詳しくわかりやすい情報が多く公開されているため他の記事を参照してください。</p>
<h2 id="Cloudflare-R2設定">Cloudflare R2設定</h2><h3 id="バケット構築">バケット構築</h3><p>まずはバケットを作ります。<br>2024&#x2F;05現在では凝った設定はできないため、ただ箱のみとなります。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">resource <span class="string">&quot;cloudflare_r2_bucket&quot;</span> <span class="string">&quot;maintenance&quot;</span> &#123;</span><br><span class="line">  account_id = local.account_id</span><br><span class="line">  name       = <span class="string">&quot;maintenance-r2&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="パブリックアクセス設定">パブリックアクセス設定</h3><p>Cloudflare R2の設定画面からパブリックアクセス設定の「ドメインに接続」をクリックし、Cloudflareで管理しているドメインを利用して任意のドメインを入力します。</p>
<img fetchpriority="high" src="/images/2024/20240531a/r2_setting_1.jpg" alt="r2_setting_1.jpg" width="1176" height="841">

<p>ステータスがアクティブになったら完了です。</p>
<img src="/images/2024/20240531a/r2_setting_2.jpg" alt="r2_setting_2.jpg" width="1200" height="291" loading="lazy">

<h3 id="htmlファイルアップロード">htmlファイルアップロード</h3><p>メンテナンスページ（htmlファイル）のデプロイは自動化したいため、APIでのアップロードを行います。</p>
<p>Cloudflare R2のAPIは、制約は多いですがAWS S3 APIと互換性があるため、AWS CLIやAWS SDKなどと同様に実行できます。</p>
<p>今回はAWS CLIで実行します。</p>
<p>まずは、APIトークンを発行する必要があります。</p>
<p>Cloudflare R2の概要画面から「R2 APIトークンの管理」をクリックし、「APIトークンを作成する」をクリックします。</p>
<img src="/images/2024/20240531a/r2_setting_3.jpg" alt="r2_setting_3.jpg" width="1165" height="367" loading="lazy">

<img src="/images/2024/20240531a/r2_setting_4.jpg" alt="r2_setting_4.jpg" width="1168" height="235" loading="lazy">

<p>オブジェクトの書き込み権限を付与して作成します。<br>また、バケットはメンテナンスページ用のバケットに特定しておきましょう。<br><img src="/images/2024/20240531a/r2_setting_5.jpg" alt="r2_setting_5.jpg" width="1150" height="838" loading="lazy"></p>
<p>作成完了すると以下の情報が表示されるため控えておきます。</p>
<ul>
<li>アクセスキーID</li>
<li>シークレットアクセスキー</li>
<li>エンドポイント</li>
</ul>
<img src="/images/2024/20240531a/r2_setting_6.jpg" alt="r2_setting_6.jpg" width="1114" height="841" loading="lazy">

<p>aws configureでアクセスキーとシークレットアクセスキーを設定し、以下のコマンドを実行します。</p>
<p><code>$HTML_PATH</code>はアップロードしたいhtmlファイルのパス、<code>$R2_BUCKET_NAME</code>はバケット名、<code>$R2_ENDPOINT</code>はエンドポイントに適宜修正してください。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">aws s3 <span class="built_in">cp</span> <span class="variable">$HTML_PATH</span> s3://<span class="variable">$R2_BUCKET_NAME</span> --endpoint-url https://<span class="variable">$R2_ENDPOINT</span> --region apac</span><br></pre></td></tr></table></figure>

<h2 id="リダイレクトルール設定">リダイレクトルール設定</h2><h3 id="許可IPリスト作成">許可IPリスト作成</h3><p>まずメンテンナンス中でもアクセスを許可したい運用保守拠点のIPアドレスリストを作成します。</p>
<p>下記のリストはTerraformからでも作れますが、著者が検証した限りでは何も変更を加えていないのにも関わらずapplyをする度にリソースが作り直されるという現象が起き、正しく設定されているのかがわかりづらく運用上困るため手動構築としています。</p>
<p>アカウント管理のリスト管理画面から、「リストを作成する」をクリックして作成します。</p>
<img src="/images/2024/20240531a/rule_setting_1.jpg" alt="rule_setting_1.jpg" width="1149" height="430" loading="lazy">

<img src="/images/2024/20240531a/rule_setting_2.jpg" alt="rule_setting_2.jpg" width="696" height="663" loading="lazy">

<p>リスト作成後、許可したいIPアドレスを追加したら完了です。</p>
<img src="/images/2024/20240531a/rule_setting_3.jpg" alt="rule_setting_3.jpg" width="1159" height="627" loading="lazy">

<h3 id="リダイレクトルール作成">リダイレクトルール作成</h3><p>作成したIPリスト以外のIPリストからのアクセスはメンテナンスページにリダイレクトするように設定します。</p>
<p><code>locals</code>で設定しているところは適宜修正してください。注意事項としては、ルールは<code>ruleset</code>という単位で作成され、リダイレクトルール全体で1つの<code>ruleset</code>である必要があります。</p>
<p>つまり、既に他のリダイレクトルールが存在していると作成できなかったり、後から別のリダイレクトルールを追加しようとすると<code>ruleset</code>ごと更新になったりする点に注意が必要です。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">resource <span class="string">&quot;cloudflare_ruleset&quot;</span> <span class="string">&quot;single_redirects&quot;</span> &#123;</span><br><span class="line">  kind    = <span class="string">&quot;zone&quot;</span></span><br><span class="line">  zone_id = local.zone_id</span><br><span class="line">  name    = <span class="string">&quot;Redirect Rules&quot;</span></span><br><span class="line">  phase   = <span class="string">&quot;http_request_dynamic_redirect&quot;</span></span><br><span class="line"></span><br><span class="line">  rules &#123;</span><br><span class="line">    action = <span class="string">&quot;redirect&quot;</span></span><br><span class="line">    action_parameters &#123;</span><br><span class="line">      from_value &#123;</span><br><span class="line">        status_code = 302</span><br><span class="line">        target_url &#123;</span><br><span class="line">          value = local.maintenance_page_url</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    expression  = <span class="string">&quot;(not ip.src in <span class="variable">$allow_ips_maintenance</span> and http.request.full_uri ne \&quot;<span class="variable">$&#123;local.maintenance_page_url&#125;</span>\&quot;)&quot;</span></span><br><span class="line">    description = <span class="string">&quot;Redirect from outside the maintenance location&quot;</span></span><br><span class="line">    enabled     = <span class="literal">false</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="なぜリダイレクトルールか">なぜリダイレクトルールか</h3><p>Cloudflareでこのようなリダイレクトを行いたいときには、他に以下の手段があります。</p>
<ul>
<li><p>Cloudflare Rules(Page Rules)</p>
</li>
<li><p>Cloudflare Workers</p>
</li>
</ul>
<p>このうちPage Rulesは最近非推奨となり、廃止されることが予定されています。</p>
<p>Workersはより細かく柔軟に設定が可能ですが、今回のような単純なリダイレクト制御の場合はそこまでは不要であるため、よりシンプルなリダイレクトルールを採用しています。</p>
<h2 id="自動化設定">自動化設定</h2><p>自動化についてはGitHub Actionsなどを想定してはしていますが、特定のサービスに依存はしないためコマンドのみ記載します。</p>
<h3 id="APIトークン作成">APIトークン作成</h3><p>まずはAPIで色々と実行するためにAPIトークンを作成します。</p>
<p>プロフィールのAPIトークン画面から「トークンを作成する」をクリックします。</p>
<img src="/images/2024/20240531a/auto_setting_1.jpg" alt="auto_setting_1.jpg" width="1200" height="271" loading="lazy">

<p>権限は動的リダイレクトの編集権限とキャッシュパージの実行権限が必要です。</p>
<p>また後述するルールセットIDの調査のためにルールセットの読み取り権限も付けておきます。</p>
<p>その他は極力必要最小権限となるように設定しましょう。</p>
<img src="/images/2024/20240531a/auto_setting_2.jpg" alt="auto_setting_2.jpg" width="820" height="840" loading="lazy">

<p>作成が完了したらトークンの値を控えておきます。</p>
<img src="/images/2024/20240531a/auto_setting_3.jpg" alt="auto_setting_3.jpg" width="946" height="439" loading="lazy">

<h3 id="メンテナンスイン">メンテナンスイン</h3><p>以下のコマンドでメンテナンスインを実現できます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="comment"># R2バケットにメンテナンスページをアップロード</span></span><br><span class="line">aws s3 <span class="built_in">cp</span> <span class="variable">$HTML_PATH</span> s3://<span class="variable">$R2_BUCKET_NAME</span> --endpoint-url https://<span class="variable">$R2_ENDPOINT</span> --region apac</span><br><span class="line"></span><br><span class="line"><span class="comment"># リダイレクトルールを有効化</span></span><br><span class="line">curl -X PATCH https://api.cloudflare.com/client/v4/zones/<span class="variable">$ZONE_ID</span>/rulesets/<span class="variable">$RULESET_ID</span>/rules/<span class="variable">$RULE_ID</span> \</span><br><span class="line">    -H <span class="string">&quot;Authorization:Bearer <span class="variable">$API_TOKEN</span>&quot;</span> \</span><br><span class="line">    -H <span class="string">&quot;Content-Type:application/json&quot;</span> \</span><br><span class="line">    -d <span class="string">&#x27;&#123;&quot;description&quot;:&quot;Redirect from outside the maintenance location&quot;,&quot;action&quot;:&quot;redirect&quot;,&quot;action_parameters&quot;:&#123;&quot;from_value&quot;:&#123;&quot;status_code&quot;:302,&quot;target_url&quot;:&#123;&quot;value&quot;: &quot;$MAINTENANCE_PAGE_URL&quot;&#125;&#125;&#125;,&quot;expression&quot;:&quot;(not ip.src in $allow_ips_maintenance and http.request.full_uri ne \&quot;$MAINTENANCE_PAGE_URL\&quot;)&quot;,&quot;enabled&quot;:true&#125;&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># キャッシュパージ</span></span><br><span class="line">curl -X POST https://api.cloudflare.com/client/v4/zones/<span class="variable">$ZONE_ID</span>/purge_cache \</span><br><span class="line">    -H <span class="string">&quot;Authorization:Bearer <span class="variable">$API_TOKEN</span>&quot;</span> \</span><br><span class="line">    -H <span class="string">&quot;Content-Type:application/json&quot;</span> \</span><br><span class="line">    -d <span class="string">&#x27;&#123;&quot;purge_everything&quot;:true&#125;&#x27;</span></span><br></pre></td></tr></table></figure>

<h3 id="メンテナンスアウト">メンテナンスアウト</h3><p>以下のコマンドでメンテナンスアウトを実現できます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="comment"># リダイレクトルールを無効化</span></span><br><span class="line">curl -X PATCH https://api.cloudflare.com/client/v4/zones/<span class="variable">$ZONE_ID</span>/rulesets/<span class="variable">$RULESET_ID</span>/rules/<span class="variable">$RULE_ID</span> \</span><br><span class="line">    -H <span class="string">&quot;Authorization:Bearer <span class="variable">$API_TOKEN</span>&quot;</span> \</span><br><span class="line">    -H <span class="string">&quot;Content-Type:application/json&quot;</span> \</span><br><span class="line">    -d <span class="string">&#x27;&#123;&quot;description&quot;:&quot;Redirect from outside the maintenance location&quot;,&quot;action&quot;:&quot;redirect&quot;,&quot;action_parameters&quot;:&#123;&quot;from_value&quot;:&#123;&quot;status_code&quot;:302,&quot;target_url&quot;:&#123;&quot;value&quot;: &quot;$MAINTENANCE_PAGE_URL&quot;&#125;&#125;&#125;,&quot;expression&quot;:&quot;(not ip.src in $allow_ips_maintenance and http.request.full_uri ne \&quot;$MAINTENANCE_PAGE_URL\&quot;)&quot;,&quot;enabled&quot;:false&#125;&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># R2バケットのメンテナンスページ削除 ※デプロイ中は常時アクセス可能となっているため削除しておきます</span></span><br><span class="line">aws s3 <span class="built_in">rm</span> s3://<span class="variable">$R2_BUCKET_NAME</span>/<span class="variable">$HTML_PATH</span> --endpoint-url https://<span class="variable">$R2_ENDPOINT</span> --region apac</span><br><span class="line"></span><br><span class="line"><span class="comment"># キャッシュパージ</span></span><br><span class="line">curl -X POST https://api.cloudflare.com/client/v4/zones/<span class="variable">$ZONE_ID</span>/purge_cache \</span><br><span class="line">    -H <span class="string">&quot;Authorization:Bearer <span class="variable">$API_TOKEN</span>&quot;</span> \</span><br><span class="line">    -H <span class="string">&quot;Content-Type:application/json&quot;</span> \</span><br><span class="line">    -d <span class="string">&#x27;&#123;&quot;purge_everything&quot;:true&#125;&#x27;</span></span><br></pre></td></tr></table></figure>

<h3 id="【補足】ルールセットID（-ルールID）の確認方法">【補足】ルールセットID（&amp;ルールID）の確認方法</h3><p>著者が調べた限りでは、画面上で簡単にルールセットIDを確認する方法がありませんでしたので、Cloudflare APIを利用して確認しています。</p>
<p>ルールIDについては、作成したルール詳細を画面で表示するとURL末尾に入っていますが、一応同様に記載します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="comment"># ルールセットIDの確認</span></span><br><span class="line">curl -X GET https://api.cloudflare.com/client/v4/zones/<span class="variable">$ZONE_ID</span>/rulesets \</span><br><span class="line">    -H <span class="string">&quot;Authorization: Bearer <span class="variable">$API_TOKEN</span>&quot;</span> \</span><br><span class="line">    -H <span class="string">&quot;Content-Type:application/json&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ルールIDの確認</span></span><br><span class="line">curl -X GET https://api.cloudflare.com/client/v4/zones/<span class="variable">$ZONE_ID</span>/rulesets/<span class="variable">$RULESET_ID</span> \</span><br><span class="line">    -H <span class="string">&quot;Authorization: Bearer <span class="variable">$API_TOKEN</span>&quot;</span> \</span><br><span class="line">    -H <span class="string">&quot;Content-Type:application/json&quot;</span></span><br></pre></td></tr></table></figure>

<h2 id="さいごに">さいごに</h2><p>Cloudflare R2とリダイレクトルールを利用してメンテナンスイン&#x2F;アウトを実装できました。</p>
<p>少しでもCloudflareを利用している方の参考になれば幸いです。</p>
]]></content>
    <summary type="html">CloudflareをCDNやDNSに利用しているサービスにおいて、CDNレイヤでメンテナンスイン/アウトを実装する方法を書いてみます。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="CDN" scheme="https://future-architect.github.io/tags/CDN/"/>
    <category term="Cloudflare" scheme="https://future-architect.github.io/tags/Cloudflare/"/>
  </entry>
  <entry>
    <title>Cloudflare採用のアーキテクチャ選定</title>
    <link href="https://future-architect.github.io/articles/20240529a/"/>
    <id>https://future-architect.github.io/articles/20240529a/</id>
    <published>2024-05-28T15:00:00.000Z</published>
    <updated>2024-05-28T15:00:00.000Z</updated>
    <author><name>宮崎将太</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240529a/image.png" alt="" width="1200" height="404">

<p>Cloudflare連載3日目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは、TIGの宮崎将太です。  </p>
<p>これまで個人&#x2F;業務ともにあまりCloudflareを使用してきませんでした。クラウド前提で仕事をしていると、各クラウドプロバイダーが提供するCDNやWAFがあり、その部分だけ外部サービスを採用するメリットを見いだせなかったことが最大の理由です。</p>
<p>しかし、とある事情からCloudflare採用に至るケースが発生したので、その動機と比較検討内容を記事にしておこうと思います。</p>
<h2 id="どんなアーキテクチャでCloudflareを採用したのか？">どんなアーキテクチャでCloudflareを採用したのか？</h2><p>結論ですが、ハイブリッドクラウド構成でのCDN&#x2F;WAFとしてCloudflareを採用しました。  </p>
<p>構成は少しぼかしますが、メインのサービス提供はAWS、認証機能の提供のみAzureを使用しています。想定するユースケースにおいてコストメリットもあるため基本的なクラウド環境にはAWSを使用しているのですが、ある特定のケースでは認証領域においてAzureの方が機能的&#x2F;コスト的優位であり、ハイブリッド構成を取っています。  </p>
<p>Cloudflareに期待する役割はCDN、WAF（ついでにDNS）であり、各サービスへのゲートウェイとして機能しています。  </p>
<img src="/images/2024/20240529a/c0453d5e-005e-585d-29ef-99f8ced603fd.png" alt="" width="1200" height="499" loading="lazy">

<h2 id="取りうるアーキテクチャの比較">取りうるアーキテクチャの比較</h2><p>さて、これだけ見るといろいろと突っ込みどころがあるかと思います。  </p>
<p>前提として、Azureの認証サービス側はWAF機能を賄ってくれなく、別途WAFと連携する必要があります。  </p>
<p>もちろん、だとしてもやたらと採用サービスを増やすのは得策ではなく、各クラウドサービスで対応できるのであれば当然そちらの方がベターです。  </p>
<p>CDN、WAF、DNSはAWS&#x2F;Azure単体でカバーもできるので、考えられる以下3案を比較してみます。  </p>
<ol>
<li>AWSとAzureをそれぞれ独立させて管理する</li>
<li>AWSかAzureをゲートウェイとし、もう片方のクラウドにルーティングさせる</li>
<li>Cloudflareをゲートウェイとし、それぞれのクラウドにルーティングさせる</li>
</ol>
<ul>
<li>別サービスとしては当社にて採用実績のあるCloudflareとAkamaiを比較しましたが、Akamaiはコスト面で圧倒的に劣る（その分機能は豊富ですが）ので今回の比較からは除外します。</li>
</ul>
<p>比較の前提として、CDN&#x2F;WAFには以下の要素を求めます。</p>
<ul>
<li>IP制限、リージョン制限等の一般的なアクセス制限機能</li>
<li>WAF保護ルールの継続的なアップデート</li>
<li>※その他機能はあれば尚良しで加点</li>
</ul>
<p>上記を前提とした場合それぞれのクラウドでの採用サービス候補は以下とし、参考のため基本的な料金を載せておきます。</p>
<ul>
<li>AWS: CloudFront + AWS WAF<ul>
<li>CloudFront 基本料金: なし,  データ転送料(エッジ): 10TBまで0.114USD&#x2F;月, データ転送料(オリジン): 0.060USD月, リクエスト料: 0.009USD&#x2F;月</li>
<li>AWS WAF 基本料金: なし, Web ACL: 5USD&#x2F;月, ルール1件あたり: 1USD&#x2F;月, リクエスト100万件あたり: USD 0.6USD&#x2F;月, マネージドルール: ものにより5USD~30USD&#x2F;月程度<ul>
<li>マネージドルールの利用料にも寄りますが、100USD&#x2F;月程度と過程します。</li>
</ul>
</li>
</ul>
</li>
<li>Azure: AzureFrontDoor Premium<ul>
<li>基本料金: 330USD&#x2F;月、データ転送量: 0.115USD&#x2F;GB (エッジからクライアント) 0.06USD&#x2F;GB (エッジから配信元)</li>
</ul>
</li>
<li>Cloudflare: Cloudflare Businessプラン<ul>
<li>料金: 250USD&#x2F;月, データ量による従量課金: なし</li>
</ul>
</li>
</ul>
<h3 id="（1）AWSとAzureをそれぞれ独立させて管理する">（1）AWSとAzureをそれぞれ独立させて管理する</h3><img src="/images/2024/20240529a/709b68d9-ec9a-4772-3e99-028227cfc9d1.png" alt="" width="922" height="538" loading="lazy">

<p>AWSではCloudFront&#x2F;AWS WAF、AzureではAzureFrontDoorを採用し、それぞれ独立したシステムとするパターンです。<br>まずはこのパターンを考えたくなると思います。  </p>
<p>他パターンと比較すると可用性ではAWS&#x2F;Azureで独立しているので、何かしら障害が起きた場合でも相互に影響することはなく、余計な心配をする必要はありません。  </p>
<p>ただし、同じCDN&#x2F;WAFという機能にそれぞれコストを支払うことになるのと、機能的にもルールのメンテナンスに不安が残るのが痛いところ。  ルールメンテナンスにWAFCharmなど外部サービスを組み合わせる手もありますが、その場合はAWS&#x2F;Azureで2つライセンスが必要になるので、更にコストがかさみます（WAFCharmは2023年の料金改定で1ライセンスあたり十数万&#x2F;月となりました。）</p>
<p>また、メンテナンスイン&#x2F;アウトなどをCDNレイヤで実施する場合はAWS&#x2F;Azureそれぞれで操作が必要になり、管理が煩雑になりがちです。  </p>
<h3 id="（2）AWSかAzureをゲートウェイとし、もう片方のクラウドにルーティングさせる">（2）AWSかAzureをゲートウェイとし、もう片方のクラウドにルーティングさせる</h3><img src="/images/2024/20240529a/eaa742af-4d9b-05f2-7afe-26d3cea905f0.png" alt="" width="1200" height="489" loading="lazy">

<p>（1）を考えた時に、CDN&#x2F;WAFのコスト&#x2F;運用を圧縮しようと思うと片方のクラウドをゲートウェイ化する案も考えられます。</p>
<p>一見あり得る構成かと思ましたが、実際に検証をしてみたところ、少なくともCloudFrontからAzure認証サービスへのルーティングはAzure側がエラーステータスを返却してしまい、テクニカルサポートに相談をしても他クラウドプロバイダーとの連携はサポート外と言われてしまいました（これ自体は言われてみれば当然かと思います）。  </p>
<p>仮に一時的に問題が解決したとしても継続的なサービス提供には不安が残るため、採用は見送りました。もちろん採用サービスによっては技術的制約が無い場合はあり、そのケースではこちらの案も取り得るかと思います。  </p>
<p>ただし、その場合でも（1）同様WAFルールセットの更新に不安は残り、外部サービスで補強しようとすると結局（3）の方が安価になる可能性はあります。  </p>
<h3 id="（3）Cloudflareをゲートウェイとし、それぞれのクラウドにルーティングさせる">（3）Cloudflareをゲートウェイとし、それぞれのクラウドにルーティングさせる</h3><img src="/images/2024/20240529a/03bf4522-e2c8-0868-f056-1e8d077d298c.png" alt="" width="1200" height="478" loading="lazy">

<p>最後に件のCloudflareをゲートウェイとするパターンです。  </p>
<p>CloudflareのWAFはCloudflare社が管理するマネージドルールの定期的なアップデートがある他、機械学習によって自動的にルールのアップデートがなされます。  </p>
<p>また、ゼロトラストやCloudflare Workersなどの機能もあり、CDN&#x2F;WAF&#x2F;エッジとしての機能は比較的豊富です。ハイブリッドクラウド構成の場合はドメインを一箇所で管理できるメリットもあります。  </p>
<p>Cloudfront&#x2F;AWS WAFと比較すると多少値が張りますが、構成によってはクラウドプロバイダーそれぞれでコストを支払うよりも安上がりになります。  </p>
<p>また、BusinessプランではSLA100％であり、この構成にした事による可用性減は発生しません。</p>
<h3 id="まとめ">まとめ</h3><p>比較検討表を記載します。  </p>
<p>（3）のパターンではCloudflare以外にも候補がある可能性はありえますが、各クラウドプロバイダーとの連携がサポートされている点は大きく評価できるかと思います。  </p>
<p>表には記載していませんが、GoogleCloudとの連携もサポートされており、様々なケースの使用が期待できそうです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th></th>
<th>AWSとAzureをそれぞれ独立させて管理する</th>
<th>AWSかAzureをゲートウェイとし、もう片方のクラウドにルーティングさせる</th>
<th>Cloudflareをゲートウェイとし、それぞれのクラウドにルーティングさせる</th>
</tr>
</thead>
<tbody><tr>
<td>概要</td>
<td>AWSではCloudFront&#x2F;AWS WAF、AzureではAzureFrontDoorを採用し、それぞれ独立したシステムとする。</td>
<td>例えばCloudFrontのオリジンとしてAzureを登録する。<br>もしくはAzureFrontDoorの配信元としてAWSを登録する。</td>
<td>CloudflareをCDN&#x2F;WAFとして使用し、AWS&#x2F;Azureにルーティングさせる。<br></td>
</tr>
<tr>
<td>機能</td>
<td>△<br>CloudFront&#x2F;AzureFrontDoorともにマネージドルールのアップデートはあるものの、頻度に不安があり、しばしばWAFCharmなどでルールセット更新を自動化することがある。</td>
<td>△<br>AWSとAzureをそれぞれ独立させて管理するパターンと同等。</td>
<td>◯<br>Cloudflareが管理するルールによって、先進のゼロデイ脆弱性保護が得られる。<br>また、WAF機械学習によりルールセットが補完される。<br>リスト型攻撃のチェックによりアカウント乗っ取りを監視&#x2F;ブロックする。<br>その他ゼロトラストやCloudflare Workersなどの機能もあり、CDN&#x2F;WAF&#x2F;エッジとしての機能は比較的豊富。<br>ドメインを一箇所で管理できるメリットもある。</td>
</tr>
<tr>
<td>可用性</td>
<td>◯<br>AWS&#x2F;Azureで独立しているので、何かしら障害が起きた場合でも相互に影響することはない。</td>
<td>△<br>ゲートウェイとなっているサービスに障害が発生すると<br>もう片方のサービスにもアクセスできなくなってしまうので、（1）と比較すると可用性は落ちる。<br>※CloudFrontは99.9％のSLAを維持する努力をするとあるが、保証するものではない。<br>    AzureFrontDoorは99.99％のSLAが保証されている。</td>
<td>◯<br>Cloudflare BusinessプランであればSLA100％が保証されるので、<br>この構成を取った事による可用性ダウンはなし。</td>
</tr>
<tr>
<td>コスト</td>
<td>△<br>AWS&#x2F;AzureそれぞれでCDN&#x2F;WAF料金を支払うことになるので、その分コスト増になる。<br></td>
<td>◯<br>AWS&#x2F;Azureどちらかにコストを集約することができる。<br></td>
<td>◯<br>Cloudflareにコストを集約することができる。<br>BusinessプランではAWS&#x2F;Azure併用よりも安価である。</td>
</tr>
<tr>
<td>運用</td>
<td>△<br>例えばメンテナンスイン&#x2F;アウトさせる場合、それぞれのクラウドでオペレーションが必要であり、運用保守者からすると習熟コストが高い。</td>
<td>◯<br>AWS&#x2F;Azureどちらかに運用を集約することができる。</td>
<td>◯<br>Cloudflare自体の習熟は必要となるが、運用作業自体はCloudflareに集約することができる。<br>また、CDN&#x2F;WAFとしてのCloudflareは比較的習得し易い仕様である。</td>
</tr>
<tr>
<td>技術的制約&#x2F;サポート</td>
<td>◯<br>AWS&#x2F;Azureで独立しているので、構築の上での技術的制約は無い。<br>テクニカルサポートに回答を断られることはない。</td>
<td>△<br>検証した限りではCloudFrontのオリジンとしてAzure認証サービスを登録した場合、Azure側でエラーが発生してしまい回避させることが不可であった。<br>他クラウドとの連携ということもあり、トラブルに際する問い合わせもたらい回しにされがち。</td>
<td>◯<br>Cloudflareが各クラウドプロバイダーとの連携を案内しており、技術的な制約はなし。<br>https://www.cloudflare.com/ja-jp/multi-cloud/aws/<br>https://www.cloudflare.com/ja-jp/multi-cloud/azure/<br>またクラウドプロバイダー側もCloudflareを連携パートナーとして案内している。</td>
</tr>
<tr>
<td>総評</td>
<td>コストと運用がネック。<br>また、WAFルールセットの更新に不安が残る。</td>
<td>採用サービスによっては技術的制約が無い場合があり、そのケースでは採用し得る。<br>WAFルールセットの更新頻度には不安が残る。</td>
<td>コストメリットが高く、ハイブリッドクラウド構成では十分に採用し得る。</td>
</tr>
</tbody></table></div>
<h2 id="さいごに">さいごに</h2><p>クラウド化が進む昨今ですが、徐々にそれぞれのクラウドプロバイダーに得意&#x2F;不得意が見えてきています。</p>
<p>もちろん各プロバイダーは不得意を補う動きもするでしょうが、ITサービスの対応領域が拡大する昨今、基本的には強みを伸ばしていく戦略を取るのではないでしょうか。  </p>
<p>そうなった場合、Cloudflareのような領域特化のクラウドサービスを組み合わせていくことも増えていくかと思いますので、同じような検討をする方の参考になれば幸いです。  </p>
]]></content>
    <summary type="html">どんなアーキテクチャでCloudflareを採用したのか？結論ですが、ハイブリッドクラウド構成でのCDN/WAFとして</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="CDN" scheme="https://future-architect.github.io/tags/CDN/"/>
    <category term="Cloudflare" scheme="https://future-architect.github.io/tags/Cloudflare/"/>
    <category term="WAF" scheme="https://future-architect.github.io/tags/WAF/"/>
    <category term="アーキテクチャ" scheme="https://future-architect.github.io/tags/%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3/"/>
  </entry>
  <entry>
    <title>Cloudflare連載を始めます &amp; WorkersにPythonをデプロイして動かしてみる</title>
    <link href="https://future-architect.github.io/articles/20240527a/"/>
    <id>https://future-architect.github.io/articles/20240527a/</id>
    <published>2024-05-26T15:00:00.000Z</published>
    <updated>2024-05-26T15:00:00.000Z</updated>
    <author><name>伊藤太斉</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240527a/CF_logo_stacked_blktype.jpg" alt="" width="1200" height="405">

<p>ロゴは https://www.cloudflare.com/ja-jp/press-kit/ より引用</p>
<p>こんにちは。TIGの伊藤です。</p>
<p>Cloudflare連載の第1日目とインデックス記事です。</p>
<h2 id="Cloudflare連載を始めます">Cloudflare連載を始めます</h2><p>CDNやインターネットセキュリティを中心としたサービスプロバイダーであるCloudflareを題材とした連載を開催します。Cloudflareについては技術ブログではこれまで、個人の寄稿で数記事上がっていましたが、連載という形にするのは今回が初めてです。</p>
<h2 id="Cloudflareとは">Cloudflareとは</h2><p>Cloudflareは、公式では以下の様に書かれていました。</p>
<blockquote>
<p>Cloudflareは、インターネット上で運営されている最大のネットワークの1つです。ユーザーは、Webサイトやサービスのセキュリティとパフォーマンスを向上させる目的でCloudflareサービスを利用しています。</p>
</blockquote>
<p>上記の様にCloudflare自体でもサービスを持ちつつ、既存のシステムのセキュリティ、パフォーマンスを向上することを目的としているサービス群です。<br>AWSなどのパブリッククラウドではリージョン、ゾーンという概念がありますが、Cloudflareでは全てエッジネットワークにて構築されており、ユーザが接続する時は一番近い接続点に繋ぎにいき、Cloudflareのサービス、その裏の他のクラウドに接続などをしています。</p>
<h2 id="連載日程">連載日程</h2><p>今回は初めての連載ということもあり、5人が参加してくれました。内容は初めて触った系の記事もありますが、わずかながら社内の知見も公開されるようなので、ぜひ楽しみにお待ちください。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>日付</th>
<th>寄稿者</th>
<th>タイトル・ネタ</th>
</tr>
</thead>
<tbody><tr>
<td>5&#x2F;27</td>
<td>伊藤太斉</td>
<td>WorkersにPythonをデプロイして動かしてみる</td>
</tr>
<tr>
<td>5&#x2F;28</td>
<td>真野隼記</td>
<td>Cloudflare D1 を触ってみる</td>
</tr>
<tr>
<td>5&#x2F;29</td>
<td>宮崎将太</td>
<td>Cloudflare採用のアーキテクチャ選定</td>
</tr>
<tr>
<td>5&#x2F;30</td>
<td>大岩潤矢</td>
<td>Cloudflare R2 + NextCloudで作る自分専用クラウドストレージのススメ</td>
</tr>
<tr>
<td>5&#x2F;31</td>
<td>小林弘樹</td>
<td>CloudflareでWebサイトのメンテナンスイン&#x2F;アウトを実装</td>
</tr>
</tbody></table></div>
<p>※公開順、日程は変わる可能性がございますが、ご了承ください。</p>
<hr>
<h2 id="Cloudflare-WorkersでPythonを使いたい">Cloudflare WorkersでPythonを使いたい</h2><p>今から約1ヶ月前、2024&#x2F;04&#x2F;02にCLoudflareのアナウンスより、Workersにオープンベータという形でPythonが利用できるようになりました。</p>
<p>https://blog.cloudflare.com/python-workers</p>
<p>本記事の結論をいきなり出しますが、PythonをWorkersで利用するにはオープンベータということもあり、まだまだ制限がありました。ただ、WorkersでPythonを使う、ということに対しては最低限であれば動くというのが分かりました。<br>個人的には、これまでWorkersのアーキテクチャからJavaScriptのみをサポートしていたこともあり、ちょっととっつきづらい部分もありましたが、まだ自分に馴染みのあるPythonが使えるようになったことで言語選択の幅が出たことは喜ばしいことですね。<br>さて、早速動かせるところまでは動かしてみましょう。</p>
<h3 id="必要なもの">必要なもの</h3><p>事前準備は以下のコマンドでwranglerコマンドを利用できるようにしておきましょう。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install wrangler --save-dev</span><br></pre></td></tr></table></figure>

<h3 id="コマンドを利用してWorkersにPythonをデプロイする">コマンドを利用してWorkersにPythonをデプロイする</h3><p>はじめにPythonのプロジェクトを作成しましょう。以下のコマンドから作成します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm create cloudflare@latest</span><br><span class="line"></span><br><span class="line">In <span class="built_in">which</span> directory <span class="keyword">do</span> you want to create your application?</span><br><span class="line"><span class="built_in">dir</span> ./cf-python <span class="comment"># 作成するディレクトリ名を記載</span></span><br><span class="line"></span><br><span class="line">What <span class="built_in">type</span> of application <span class="keyword">do</span> you want to create?</span><br><span class="line"><span class="built_in">type</span> <span class="string">&quot;Hello World&quot;</span> Worker (Python) <span class="comment"># PythonのWorkersプロジェクトを作成</span></span><br><span class="line"></span><br><span class="line">Do you want to use git <span class="keyword">for</span> version control?</span><br><span class="line"><span class="built_in">yes</span> git <span class="comment"># Gitのバージョンコントロールをするか</span></span><br><span class="line"></span><br><span class="line">Do you want to deploy your application?</span><br><span class="line"><span class="built_in">yes</span> deploy via `npm run deploy` <span class="comment"># Workersに初期プロジェクトをデプロイするか</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ここでブラウザ側で認証が走り、認証後デプロイされる</span></span><br></pre></td></tr></table></figure>

<p>デプロイまで完了すると、以下の画面に遷移します。</p>
<img src="/images/2024/20240527a/スクリーンショット_2024-05-26_21.59.14.png" alt="スクリーンショット_2024-05-26_21.59.14.png" width="1200" height="775" loading="lazy">

<p>この時デプロイされているソースコードは<code>./src/entry.py</code>に格納されています。</p>
<figure class="highlight py"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> js <span class="keyword">import</span> Response</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">on_fetch</span>(<span class="params">request, env</span>):</span><br><span class="line">    <span class="keyword">return</span> Response.new(<span class="string">&quot;Hello World!&quot;</span>)</span><br></pre></td></tr></table></figure>

<p>ここまででの作業でPythonのアプリケーションが動く様になったので、次はPythonのパッケージを入れて試してみます。</p>
<h3 id="Pythonのパッケージのサポート">Pythonのパッケージのサポート</h3><p>2024&#x2F;04&#x2F;02時点でWorkersがサポートしているPythonのパッケージですが、公式のPackagesのページに記載があります。しかし、</p>
<blockquote>
<p>Python Workers are in open beta.<br>You can currently only use built-in packages in local development. Support for deploying packages with a <code>requirements.txt</code> file is coming soon.</p>
</blockquote>
<p>と書いてある様にPythonの標準パッケージは使えるものの、外部のパッケージについては現時点ではデプロイできない状態です。実際に試してみましたが、エラーとなり、その文面にもまだサポートされていないことが書かれていました。</p>
<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">fastapi</span><br></pre></td></tr></table></figure>

<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 実行コマンド</span></span><br><span class="line">wrangler deploy</span><br><span class="line"> ⛅️ wrangler 3.57.1</span><br><span class="line">-------------------</span><br><span class="line">▲ [WARNING] The entrypoint src/entry.py defines a Python worker, support <span class="keyword">for</span> Python workers is currently experimental.</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Attaching additional modules:</span><br><span class="line">┌─────────┬────────────────────┬──────┐</span><br><span class="line">│ Name    │ Type               │ Size │</span><br><span class="line">├─────────┼────────────────────┼──────┤</span><br><span class="line">│ fastapi │ python-requirement │      │</span><br><span class="line">└─────────┴────────────────────┴──────┘</span><br><span class="line">Total Upload: 0.06 KiB / gzip: 0.07 KiB</span><br><span class="line"></span><br><span class="line">✘ [ERROR] A request to the Cloudflare API (/accounts/xxxxxxxxxxxxxxxx/workers/scripts/yyyyyyyyy) failed.</span><br><span class="line"></span><br><span class="line">  You cannot yet deploy Python Workers that depend on packages defined <span class="keyword">in</span></span><br><span class="line">  requirements.txt. Support <span class="keyword">for</span> Python packages is coming soon. [code: 10021]</span><br><span class="line"></span><br><span class="line">  If you think this is a bug, please open an issue at:</span><br><span class="line">  https://github.com/cloudflare/workers-sdk/issues/new/choose</span><br></pre></td></tr></table></figure>

<p>実際に色々動かしてみたかったところではありますが、ネイティブでもっと動かせる時が来たらさらに試してみようと思います。</p>
<h2 id="まとめ">まとめ</h2><p>Cloudflare連載のインデックス記事とWorkersでPythonがオープンベータで使えるようになったのでできるところまで試してみた記事でした。</p>
<p>私が他の記事を見てみた時の違いは、<code>npm create</code>コマンドを実行した時に、テンプレでPythonが選べる様になっていたので、わずかながらGAになる様に進んでいそうにも見えました。今後のリリースにも期待ですね！</p>
<p>本日からあと4記事ほど続くCloudflare連載ですが、ぜひ他の記事も読んでみてください！</p>
]]></content>
    <summary type="html">Cloudflareは、インターネット上で運営されている最大のネットワークの1つです。ユーザーは、Webサイトやサービスのセキュリティとパフォーマンスを向上させる目的でCloudflareサービスを利用しています。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="Cloudflare" scheme="https://future-architect.github.io/tags/Cloudflare/"/>
    <category term="Cloudflare Workers" scheme="https://future-architect.github.io/tags/Cloudflare-Workers/"/>
    <category term="Python" scheme="https://future-architect.github.io/tags/Python/"/>
    <category term="インデックス" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9/"/>
  </entry>
  <entry>
    <title>Real World HTTPの第3版ができあがりました</title>
    <link href="https://future-architect.github.io/articles/20240513a/"/>
    <id>https://future-architect.github.io/articles/20240513a/</id>
    <published>2024-05-12T15:00:00.000Z</published>
    <updated>2024-05-12T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[
<img fetchpriority="high" src="/images/2024/20240513a/PXL_20240404_001054780.jpg" alt="" width="1200" height="901">


<p>https://www.oreilly.co.jp/books/9784814400669/</p>
<p>ひとえに読者の皆さんが買ってくれたおかげで、Real World HTTPを改訂し、このたび3版を上梓しました。ありがとうございます。2016年ごろから書き始めて、2017年に初版を出版したので、執筆段階からすると8年ほど経過しているのですが、これだけ長くこの本に関わり続けられるというのは、本書を買ってくださるみなさまのおかげです。</p>
<p>今回は、ひさびさに無料のミニ版も更新しました。本日、このブログと同時にリリースしました。よりミニ版が学習コンテンツとして使いやすくなるように、そもそもブラウザってどんな動きをするの？ というイントロの章をミニ版とオリジナル版に追加しました。</p>
<p>また、オリジナル版だけになりますが、HTTPが単なるブラウザとの通信を超えてプラットフォーム API化していっている流れに合わせて、既存のOpenSocialの話を独立させて、auさんにも取材させていただいてスーパーアプリの話を入れました。本当は他にも内容を教えてくれたけど会社NGが出て、内容は反映したけどお名前を紹介できなかったスーパーアプリもあったりしました。そんな感じで出せない内容もあったのですが、書いていていろいろ刺激を受けることができて楽しかったです（会社がわからない形で文には混ぜてあります）。</p>
<p>あとは、読書会とかで「どこまで読んだ？」がわかりやすいように、この技術ブログを参考に章の最初にアイキャッチ画像をつけるなどしました。その中で何社か問い合わせを（編集の瀧澤さんがして）許諾を取るなどもやりました。一応、著作権がある某章の画像はSNS等にあげないようにお願いします。2章じゃない方。</p>
<h2 id="AI時代の本のあり方">AI時代の本のあり方</h2><p>AI時代になって新しいサービスが雨後の筍の如くリリースされる日々です。最近リリースされたGoogle Gemini Proだと無料でも扱えるトークン数が多くなって、PDFを丸ごと読み込ませるというのがやりやすい時代になりました。おそらく、きっとそのうち、生成AIに要約させたんだろうな、と思われるような読書感想ブログがポツポツ出てくるのだろうな、という気がしています。</p>
<p>先日知り合いから聞いたのは、入力のバリデーションにassertを使った同僚がいた、みたいな話でした。assertはいろんな言語が持っている機能ですが、基本的にあり得ない状況に陥った時にシステムを止めるブレーカーのようなものです。そんでもって、開発中はいいのですが、本番環境になるとassertは取り除かれて動かなくなり、バリデーションが一切存在しないプログラムとして本番稼働することになります。</p>
<p>こういう実際の失敗談を集めた、実装でやってしまうかもしれないミスを先回りして「こういうことやっちゃダメだよ」みたいなのを集めた、親父の小言集としてもっと発展させていきたいな、と思って書いています。これはこのReal World HTTPに限らず、Goならわかるシステムプログラミングでも、（増刷があるなら）実用Goでも、他の翻訳書の脚注部分でもやっていきたいと思っています。</p>
<p>そもそも失敗系の話、あるいは時代が変わって今では不要や非推奨になった機能の話なんかは生成AIに聞いても理解が浅いなと思うことも多いですし、僕のこめた目次より小さい粒度のこだわりの話も、おそらくAIの要約では省かれてしまう部分かなと思います。もちろん、トークンの中には入っているはずなので聞けば出てくるのでしょうけど、人が読むと得られるが、生成AIで楽しようとするとスルーされて得られない情報、みたいな感じになるんじゃないかなと。そういう感じで「AIではなく人間が頑張ることで価値」が得られる演出は今後本を書く上では意識してみようかと思いました。</p>
<p>もちろん、AIを使わないメリットの話だけをするつもりはありません。前書きにも書きましたが、そのうちやってもいいかもと思っていたGo以外のサンプルなんかはAIにお任せすれば一発です。本書ではおそらく読者にしつこいと思われている（かもしれない）ぐらいcurlのコマンドを紹介していますが、curlコマンドって生成AIにHTTPリクエストの情報を構造化して伝えるにはすごい有用なんですよね。ここからRust版のサンプル作って、とか、自分の環境にあわせた読書法というのがやりやすくなりました。クライアントコードだけではなく、このリクエストを受けるサーバーコードを作らせるのも一瞬です。あと、本書はページ数を収めるために（これでも）、機能の概要しか紹介できていない項目もたくさんあります。そういうのは「もっと詳しく教えて」「もっと詳しい解説が書かれているページを教えて」みたいに問い合わせると、より深く理解できると思います。</p>
<p>そのようなAI時代の読書体験がしやすい書籍になっているのでは、と思いますので、ウェブとかHTTPとか興味はないが、新しい体験をぜひしてみたいという人も一人10冊ぐらい買っていただけるとよいのではないかと思います。</p>
<h2 id="今後の発展">今後の発展</h2><p>内容としては大きなところはだいぶ落ち着いたかな、と思います。というのも、初版の時にはまだHTTP&#x2F;2が策定されたばかりで、HTTP&#x2F;3はRFCにはなっていなかったものの、その前身のgQUICは存在していましたし、その要素技術のTLS 1.3も策定中でした。2版では策定されたばかりのTLS 1.3を取り上げ、今回はRFC化したHTTP&#x2F;3を取り上げることができたので、そう言う意味では初版で見えていた未来に辿り着いた、と言う感じはあります。</p>
<p>ウェブの新しい技術として出てくるものは、流行るか流行らないのかがわからないものが数多くあります。SPDYみたいな約束された未来みたいなものは当初から扱っていましたが、本書では基本的に「すでに普及したもの」に限定しています。過去の版では流行ると思って紹介したけど、今回削った内容もぽつぽつあったりします。</p>
<p>技術的にはWebTransportみたいな、本書ではまだ概要しか触れていないものとか、まだまだ書きたい内容はこれからも出てくるはずです。もともと本書を書く動機となったのは、ウェブサービスのプログラミングをしていて、そのなかで疑問に思って調べた細かくちらばっていた情報をまとめたい、というところからでした。僕自身は初版を書き終えた時点でウェブサービスを作る上で（HTTPの知識不足で）困ることはだいぶ減りましたが、仕事の中でいろいろな人の相談に乗っていて「あ、こういうところでつまづいていたのか」といった内容がどんどんネタ帳に溜まってきて、改訂のタイミングで盛り込む、という感じで版を重ねています。実施、今回増えた内容も、決して新しいから増えたというものだけではありません。</p>
<p>自分自身も別にHTTPの専門家ではないとはずっと思っていますし、ブラウザ実装者の人とかが書いてくれたらいいなー、でも出ないから仕方なく自分で書くか、みたいな気持ちはずっとあったのですが、先日編集の瀧澤さんから「一連のやり取りをみていて、改めてリアルワールドのHTTPに関する本を渋川さんが書かれてる意味がわかる気がする」と言うコメントを（まったく違う文脈の中で）いただきました。</p>
<p>一次資料を読め、一次資料が絶対だ、というのはIT業界ではいろいろなところで聞く言葉ではあります。この本は一次資料ではありません。ほとんどの内容はRFCを読めば書いてありますが、実際にコードを書いていて、周りの若者がハマった内容とか、そういうのを拾い上げて、これから学習していく自分よりも若い人たちが楽して多くの経験が得られる知識の高速道路本にしていきたいという気持ちは持ち続けています。</p>
<p>書籍を読んだら、その感想をブログにして欲しい、みたいな話はよく出版ブログでは見ますが、前述のような方向性で今後も発展させていきたいので、本書の場合は読んでしばらくしたあとに「あー、これが書いてあって役にたったわー」みたいなSNSのつぶやきもありがたいです。あるいは、若者にウェブ技術を教えていて、前述のassertとか、今回追加したredirect&#x2F;rewriteの話とか、若者がこんな誤解をしていた！ みたいな話も大好物です。Real World HTTPという書名付きでつぶやいてもらえたら拾いに行きます。</p>
<h2 id="No-1">No.1</h2><p>オライリーには数多くの本がありますが、日本語書き下ろしで3版まで進んでいるのは現在のところ、Sphinxをはじめよう 第3版と、本書のみになります。もちろん、英語原著で改版の回数が多い本とか、増刷の回数とか、もろもろだともっと上の本もありますし、続巻スタイルだとゼロから作るDeep Learningシリーズパイセンが圧倒的です。それでも、まあとりあえず（日本語書き下ろしでの改版数がオライリーで）トップタイで日本一、単著だと堂々の日本一ということになります(分野を小さくして一位を作り出すという姑息な手法はビジネス書的なテクでありますが)。どちらの本も編集者は瀧澤さんです。書籍だけでなく、仕事もそうですが、一緒に関われてよかったと思ってもらえるような実績を周りの人には残せたらな、と思っていたので、これも今回良かったな、と個人的に思っている点です。読者の皆さんには関係のない話ではありますが。</p>
<h2 id="One-More-Thing…">One More Thing…</h2><p>もう一冊、並行して書いていたPlaywrightのWebフロントエンドのE2Eテスト本も予約を開始しています。知り合いのテスト系の有名な方々にもレビューしてもらったり、こちらも良い経験ができました。こちらの詳細はまた別途他のメンバーが書いてくれると思います。</p>
]]></content>
    <summary type="html">ひとえに読者の皆さんが買ってくれたおかげで、Real World HTTPを改訂し、このたび3版を上梓しました。ありがとうございます。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="HTTP" scheme="https://future-architect.github.io/tags/HTTP/"/>
    <category term="O'Reilly" scheme="https://future-architect.github.io/tags/O-Reilly/"/>
    <category term="RealWorldHTTP" scheme="https://future-architect.github.io/tags/RealWorldHTTP/"/>
    <category term="Web" scheme="https://future-architect.github.io/tags/Web/"/>
    <category term="出版" scheme="https://future-architect.github.io/tags/%E5%87%BA%E7%89%88/"/>
  </entry>
  <entry>
    <title>シェルスクリプトで固定長ファイルに区切り文字を入れてCSVに変換する</title>
    <link href="https://future-architect.github.io/articles/20240425a/"/>
    <id>https://future-architect.github.io/articles/20240425a/</id>
    <published>2024-04-24T15:00:00.000Z</published>
    <updated>2024-04-24T15:00:00.000Z</updated>
    <author><name>山下雄大</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240425a/29069589_s.jpg" alt="" width="640" height="480">

<p>春の入門連載の12本目です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは。新卒2年目の山下です。</p>
<p>最近ちょっとした作業でシェルスクリプトを触る機会が増え、「固定長ファイルをCSVに変換する」といったややマニアックな文字列操作をすることがありました。</p>
<p>せっかくの機会ですので、利用コマンドやオプションをまとめました。</p>
<h2 id="本編">本編</h2><h3 id="環境">環境</h3><ul>
<li>Red Hat Enterprise Linux release 8.5 (Ootpa)<br>※利用する環境によってはマルチバイト文字を扱う際の挙動が異なる可能性があります。</li>
</ul>
<h3 id="利用するファイル">利用するファイル</h3><p>例として、固定長Shift_JIS（SJIS）のファイルを可変調UTF-8に変換します。</p>
<p>入力の固定長ファイルは次の条件のものを想定しています。</p>
<ul>
<li>下記エスケープが必要な文字が含まれない<ul>
<li>改行コード（\nや\rなど）</li>
<li>カンマ（,）</li>
<li>クォート（”）</li>
</ul>
</li>
</ul>
<p><strong>変換前</strong></p>
<ul>
<li>文字コード：SJIS</li>
<li>レコード長：10Byte</li>
<li>カラム長：2Byte（全カラム固定）</li>
<li>※上記エスケープ文字や特殊文字の一部には対応出来ません</li>
</ul>
<figure class="highlight console"><figcaption><span>hoge.txt</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> hoge.txt</span></span><br><span class="line">ｱｲｳｴｵｶｷｸケ01234567８ｻｼｽｾｿﾀﾁﾂテabcdefghア</span><br></pre></td></tr></table></figure>

<p><strong>変換後</strong></p>
<ul>
<li>文字コード：UTF-8</li>
<li>CSV</li>
</ul>
<figure class="highlight console"><figcaption><span>result.csv</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> result.csv</span></span><br><span class="line">ｱｲ,ｳｴ,ｵｶ,ｷｸ,ケ</span><br><span class="line">01,23,45,67,８</span><br><span class="line">ｻｼ,ｽｾ,ｿﾀ,ﾁﾂ,テ</span><br><span class="line">ab,cd,ef,gh,ア</span><br></pre></td></tr></table></figure>

<h3 id="（1）文字コードの変更">（1）文字コードの変更</h3><p>はじめに、Bashで適切に文字を読み込めるよう、文字コードを変換します。</p>
<p><code>iconv</code>コマンドを利用し<code>-f</code>で指定した文字コードを<code>-t</code>で指定した文字コードに変換し標準出力できます。</p>
<ul>
<li>変換前のファイル</li>
</ul>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> hoge.txt <span class="comment">#-- SJISをUTF-8と解釈して出力しているため文字化けしている</span></span></span><br><span class="line">���������P01234567�W�������eabcdefgh�A</span><br></pre></td></tr></table></figure>

<ul>
<li>iconvで文字コードを変換後のファイル</li>
</ul>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">iconv -f SJIS -t UTF8 hoge.txt &gt;&gt; hogehoge.txt <span class="comment">#--リダイレクトすることでhogehoge.txtに出力</span></span></span><br><span class="line">ｱｲｳｴｵｶｷｸケ01234567８ｻｼｽｾｿﾀﾁﾂテabcdefghア</span><br></pre></td></tr></table></figure>

<h3 id="（2）改行付与">（2）改行付与</h3><h4 id="fold">fold</h4><p>最も基本的な改行付与のコマンドは<code>fold</code>コマンドです。</p>
<p>指定した値で改行を増やし標準出力します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">fold</span> -option filepath</span><br></pre></td></tr></table></figure>

<div class="scroll"><table>
<thead>
<tr>
<th>オプション</th>
<th>概要</th>
</tr>
</thead>
<tbody><tr>
<td>-b</td>
<td>バイト数で数える</td>
</tr>
<tr>
<td>-w</td>
<td>幅で数える</td>
</tr>
</tbody></table></div>
<h5 id="バイト数区切りで改行付与する場合">バイト数区切りで改行付与する場合</h5><p><code>-b</code>で指定したバイト数単位で改行が付与されます。</p>
<p>今回の例の場合、前段で文字コード変換（SJIS→UTF-8）をしたため、<br>半角カナ<code>SJIS：1Byte</code>や全角文字<code>SJIS：2Byte</code>のほとんどは<code>UTF-8：3Byte</code>にByte数が変更されるため、カラムずれがおきています。</p>
<p>（現環境では文字の途中で改行コードが挿入されることはなく、10Byteを超える場合は手前の文字までが1行となっています）。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">fold</span> -b10 hogehoge.txt</span></span><br><span class="line">ｱｲｳ</span><br><span class="line">ｴｵｶ</span><br><span class="line">ｷｸケ0</span><br><span class="line">1234567８</span><br><span class="line">ｻｼｽ</span><br><span class="line">ｾｿﾀ</span><br><span class="line">ﾁﾂテa</span><br><span class="line">bcdefghア</span><br></pre></td></tr></table></figure>

<h5 id="文字列幅区切りで改行付与する場合">文字列幅区切りで改行付与する場合</h5><p>文字列幅は<code>全角：2</code>、 <code>半角：1</code>でカウントされます。</p>
<p>SJISの半角文字は<code>1文字:1Byte</code>、<code>全角:2Byte</code>ため、文字列幅基準で改行付与することでUTF-8に変換した後でも想定するレコード長で区切ることが出来ました。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">fold</span> -w10 hogehoge.txt</span></span><br><span class="line">ｱｲｳｴｵｶｷｸケ</span><br><span class="line">01234567８</span><br><span class="line">ｻｼｽｾｿﾀﾁﾂテ</span><br><span class="line">abcdefghア</span><br></pre></td></tr></table></figure>

<h5 id="文字数区切りで改行付与する場合">文字数区切りで改行付与する場合</h5><p>参考程度ですが、文字数区切りで改行を付与する方法も記載します。</p>
<p><code>grep -o</code>を利用して<code>.</code>に当てはまる文字を1文字ずつ出力し、指定の文字数で改行を付与しています。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="keyword">for</span> char <span class="keyword">in</span> $(grep -o . hogehoge.txt) ; <span class="keyword">do</span> <span class="built_in">echo</span> -n <span class="variable">$char</span>; count=$(( count + <span class="number">1</span> )); <span class="keyword">if</span> [ <span class="variable">$count</span> -eq 9 ]; <span class="keyword">then</span> <span class="built_in">echo</span> <span class="string">&quot;&quot;</span>; count=0; <span class="keyword">fi</span>; <span class="keyword">done</span></span></span><br><span class="line">ｱｲｳｴｵｶｷｸケ</span><br><span class="line">01234567８</span><br><span class="line">ｻｼｽｾｿﾀﾁﾂテ</span><br><span class="line">abcdefghア</span><br></pre></td></tr></table></figure>

<h3 id="（3）カラム区切り文字の挿入">（3）カラム区切り文字の挿入</h3><p>次に区切り文字を挿入してカラムを分割します。</p>
<h4 id="カラム単位で個別区切り">カラム単位で個別区切り</h4><p><code>sed</code>コマンドの<code>-e</code>オプションを利用して指定した文字数で個別に<code>,（カンマ）</code>を挿入する方法です。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="keyword">while</span> <span class="built_in">read</span> line ;<span class="keyword">do</span> <span class="built_in">echo</span> <span class="variable">$line</span> | sed -e <span class="string">&#x27;s/./&amp;,/8&#x27;</span> -e <span class="string">&#x27;s/./&amp;,/6&#x27;</span> -e <span class="string">&#x27;s/./&amp;,/4&#x27;</span> -e <span class="string">&#x27;s/./&amp;,/2&#x27;</span> ; <span class="keyword">done</span> &lt; hogehoge.txt</span></span><br><span class="line">ｱｲ,ｳｴ,ｵｶ,ｷｸ,ケ</span><br><span class="line">01,23,45,67,８</span><br><span class="line">ｻｼ,ｽｾ,ｿﾀ,ﾁﾂ,テ</span><br><span class="line">ab,cd,ef,gh,ア</span><br></pre></td></tr></table></figure>

<h4 id="正規表現を利用した文字数区切り">正規表現を利用した文字数区切り</h4><p>文字列を1文字ずつループしながら下記処理を実行しています。</p>
<ol>
<li>全半角を識別</li>
<li>半角文字の場合は2文字、全角文字の場合は1文字間隔で区切り文字<code>,（カンマ）</code>を挿入</li>
<li>行の末尾で改行</li>
</ol>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="keyword">while</span> <span class="built_in">read</span> line</span><br><span class="line"><span class="keyword">do</span></span><br><span class="line">  count=0</span><br><span class="line">  <span class="keyword">for</span> ((i=<span class="number">0</span>; i&lt;<span class="variable">$&#123;#line&#125;</span>; i++)); <span class="keyword">do</span></span><br><span class="line">    char=<span class="string">&quot;<span class="variable">$&#123;line:$i:1&#125;</span>&quot;</span></span><br><span class="line">    <span class="keyword">if</span> [[ <span class="variable">$&#123;#line&#125;</span> = $(( i + <span class="number">1</span> )) ]]; <span class="keyword">then</span></span><br><span class="line">      <span class="built_in">echo</span> <span class="variable">$char</span></span><br><span class="line">      <span class="built_in">continue</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">    <span class="keyword">if</span> [[ -n $(<span class="built_in">echo</span> <span class="variable">$char</span> | grep -P <span class="string">&quot;[ｦ-ﾝ]&quot;</span>) ]] || [[ -n $(<span class="built_in">echo</span> <span class="variable">$char</span> | grep -E <span class="string">&quot;[0-9A-Za-z]&quot;</span>) ]]; <span class="keyword">then</span> <span class="comment">#ASCII or 半角カナは2文字カウントしたらカンマを挿入</span></span><br><span class="line">        count=$((count + <span class="number">1</span>))</span><br><span class="line">      <span class="keyword">if</span> [[ <span class="variable">$count</span> = 2 ]]; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> -n <span class="string">&quot;<span class="variable">$char</span>,&quot;</span></span><br><span class="line">        count=0</span><br><span class="line">      <span class="keyword">else</span></span><br><span class="line">        <span class="built_in">echo</span> -n <span class="variable">$char</span></span><br><span class="line">      <span class="keyword">fi</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">      <span class="built_in">echo</span> -n <span class="string">&quot;<span class="variable">$char</span>,&quot;</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">  <span class="keyword">done</span></span><br><span class="line"><span class="keyword">done</span> &lt; hogehoge.txt &gt; result.csv</span><br><span class="line"></span><br><span class="line">------ 結果 ------</span><br><span class="line">ｱｲ,ｳｴ,ｵｶ,ｷｸ,ケ</span><br><span class="line">01,23,45,67,８</span><br><span class="line">ｻｼ,ｽｾ,ｿﾀ,ﾁﾂ,テ</span><br><span class="line">ab,<span class="built_in">cd</span>,ef,gh,ア</span><br></pre></td></tr></table></figure>

<p>ようやくCSVに変換することが出来ました。</p>
<h2 id="まとめ">まとめ</h2><p>Bashの標準機能のみで全半角混在の文字列をキレイに分割することは少し難しいですが、簡単なレイアウトであれば上記を流用することでカラム分割が可能になりました。</p>
<p>また、今回調べる中で利用するバージョンやディストリビューションによって挙動が異なる事を知りました。</p>
<p>例えばfoldコマンドを使った改行をUbuntuで試すと、下記のように文字化けが起こります（マルチバイト文字の扱いが異なることが原因のようです）。</p>
<ul>
<li><code>Description:    Ubuntu 22.04.2 LTS</code></li>
</ul>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">fold</span> -w10 hogehoge.txt</span></span><br><span class="line">ｱｲｳ�</span><br><span class="line">��ｵｶ�</span><br><span class="line">�ｸｹｺ</span><br><span class="line">0123456789</span><br><span class="line">ｻｼｽ�</span><br><span class="line">��ｿﾀ�</span><br><span class="line">�ﾂﾃﾄ</span><br><span class="line">abcdefghij</span><br></pre></td></tr></table></figure>

<p>今回の手法は汎用的に利用できるものでは無いかも知れませんが、マルチバイト文字のByte数が変わると分割や文字カウント方法が少し複雑になり単純計算できない事も多くまとまった内容は少ないです。参考になることがあれば嬉しいです。</p>
<h2 id="参考">参考</h2><ul>
<li>https://future-architect.github.io/articles/20210329/</li>
<li>https://www.gnu.org/software/grep/manual/grep.html</li>
<li>https://gihyo.jp/book/2017/978-4-7741-8694-8</li>
</ul>
<p>アイキャッチ画像は磯の香り - No: 29069589｜写真AC を利用させていただきました。</p>
]]></content>
    <summary type="html">最近ちょっとした作業の中でシェルスクリプトを触る機会が増え、「固定長ファイルをCSVに変換する」といったややマニアックな文字列操作時もすることがありました。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="CSV" scheme="https://future-architect.github.io/tags/CSV/"/>
    <category term="Linux" scheme="https://future-architect.github.io/tags/Linux/"/>
    <category term="ShellScript" scheme="https://future-architect.github.io/tags/ShellScript/"/>
  </entry>
  <entry>
    <title>Systemdにおけるservice unitの起動フロー入門</title>
    <link href="https://future-architect.github.io/articles/20240412a/"/>
    <id>https://future-architect.github.io/articles/20240412a/</id>
    <published>2024-04-11T15:00:00.000Z</published>
    <updated>2024-04-11T15:00:00.000Z</updated>
    <author><name>森大作</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240412a/IMG_8598.jpg" alt="" width="1200" height="831">

<p>春の入門連載2024 の4本目です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは、最近寒暖差が激しくて体調崩しがちなTIGの森です。</p>
<p>入門向け記事としてLinuxの <code>systemd</code> における <code>service unit</code> の起動と停止のフローについて説明します。</p>
<h2 id="service-unit-とは">service unit とは</h2><p><code>service unit</code> とは <code>systemd</code> の設定単位の1つで、Linuxにおけるサービスの振る舞いを定義する設定をまとめたものです。主にデーモンやその他のシステムプロセスといった、起動や停止を含む管理をします。</p>
<h2 id="サービス起動の大まかな流れ">サービス起動の大まかな流れ</h2><p>サービスの起動のおおまかな流れを説明します。</p>
<p>ここでは話を簡単にするため、<code>Type</code>ディレクティブ(後述)がデフォルトの <code>simple</code> に指定されているとします。</p>
<h3 id="サービス起動の流れ">サービス起動の流れ</h3><ol>
<li><code>systemctl start [サービス名].service</code>を実行すると、<code>systemd</code>は指定された<code>service unit</code>を起動する</li>
<li><code>systemd</code>は<code>service unit</code>の設定ファイル(<code>.service</code>)を確認し、サービスが有効かどうかなどを調べる</li>
<li><code>systemd</code>が<code>fork()</code>を実行し、成功した時点で<code>service unit</code>が<code>active</code>になる。この新しく生成されたプロセスはサービスを起動するためのプロセスとなる</li>
<li>この新しいプロセスが必要な環境設定（環境変数の設定など）を行った後、<code>ExecStart</code>で指定されたプログラムを<code>execve()</code>で実行する</li>
<li>サービスのプログラムが実行され、サービスが開始する</li>
</ol>
<p>以上がざっとした流れです。もちろんLinuxにおけるサービス管理全てを網羅しているということではありません。初期化だけを実施する<code>iptables</code>などの例外もあります。</p>
<h2 id="サービス起動・終了時の前後の処理に関して">サービス起動・終了時の前後の処理に関して</h2><p>サービス実行の主要なコマンドの前に環境変数の設定や通信路の準備や初期化などを実施する場合、<code>ExecStart</code>の前処理として実行される<code>ExecStartPre</code>や、<code>active</code>となった時点で実行される<code>ExecStartPost</code>、また終了時に実行される<code>ExecStopPost</code>などのディレクトリが用意されています。</p>
<p>サービス起動・終了に関連するディレクティブは下記です。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>名称</th>
<th>概要</th>
</tr>
</thead>
<tbody><tr>
<td><code>ExecStartPre</code></td>
<td>依存関係を満たすためのリソースの確保、前段処理を実施する</td>
</tr>
<tr>
<td><code>ExecStart</code></td>
<td>環境変数を読み込み、サービス処理のプログラムを実行する</td>
</tr>
<tr>
<td><code>ExecStartPost</code></td>
<td><code>active</code>になる前のタイミングで実施する後処理を実施する</td>
</tr>
<tr>
<td><code>ExecStop</code></td>
<td>サービス停止のプログラムを実施する</td>
</tr>
<tr>
<td><code>ExecStopPost</code></td>
<td>サービス終了後の後処理を実施する。<code>inactive</code>になる前の実施する</td>
</tr>
<tr>
<td><code>ExecReload</code></td>
<td>サービスのリロードを実施する</td>
</tr>
</tbody></table></div>
<h2 id="サンプル">サンプル</h2><p>例として、<code>ExecStartPre</code>で実際にどのような前処理が実施できるか試してみましょう。</p>
<p>EC2のインスタンスのメタデータから自身のリージョンを取得し、それをサービスから呼び出して出力させます。</p>
<h3 id="手順">手順</h3><ol>
<li><p>環境変数の設定ファイル<code>/etc/environment</code>に下記の行を追加して、デフォルトのリージョンを指定</p>
 <figure class="highlight bash"><figcaption><span>/etc/environment</span></figcaption><table><tr><td class="code"><pre><span class="line">Region=default</span><br></pre></td></tr></table></figure>
</li>
<li><p><code>ExecStartPre</code>で叩かれる事によって自分のリージョンを取得するシェル<code>/etc/setEnvConf.sh</code>を作成</p>
 <figure class="highlight bash"><figcaption><span>/etc/setEnvConf.sh</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line">REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed -e <span class="string">&#x27;s/.$//&#x27;</span>)</span><br><span class="line"><span class="built_in">sudo</span> sed -i <span class="string">&quot;/^Region=/c\Region=<span class="variable">$REGION</span>&quot;</span> /etc/environment</span><br></pre></td></tr></table></figure>
</li>
<li><p><code>/etc/systemd/system</code>配下に<code>test.service</code>を作成</p>
 <figure class="highlight ini"><figcaption><span>test.service</span></figcaption><table><tr><td class="code"><pre><span class="line"> <span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Test Service</span><br><span class="line"><span class="attr">After</span>=network.target</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">EnvironmentFile</span>=/etc/environment</span><br><span class="line"><span class="attr">ExecStartPre</span>=/bin/bash /etc/setEnvConf.sh</span><br><span class="line"><span class="attr">ExecStart</span>=/bin/bash -c <span class="string">&#x27;echo &quot;The region of this instance is $&#123;Region&#125;&quot; &gt; /tmp/output.txt&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br></pre></td></tr></table></figure>
</li>
<li><p>サービスファイルを読み込んで起動</p>
 <figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl daemon-reload</span><br><span class="line"><span class="built_in">sudo</span> systemctl start <span class="built_in">test</span></span><br></pre></td></tr></table></figure></li>
</ol>
<p>以上が手順になります。<br>サービスが無事起動したら<code>/tmp/output.txt</code>を見てみましょう。</p>
<figure class="highlight txt"><figcaption><span>/tmp/output.txt</span></figcaption><table><tr><td class="code"><pre><span class="line">The region of this instance is ap-northeast-1</span><br></pre></td></tr></table></figure>

<p>ちゃんと自身のインスタンスのリージョンが取得できています。</p>
<p>災対環境などでAMIからインスタンスを起動する時に自身のリージョンを取得して、処理に繋げる際などに応用できるでしょう。</p>
<h2 id="注意点">注意点</h2><p>今回は触れませんでしたが<code>unit</code> が<code>active</code>や<code>inactive</code>になるタイミングは <code>Type</code> ディレクティブの指定によって微妙に変化します。</p>
<p>この記事ではデフォルトの<code>simple</code>を用いていますが、これは代表プロセスが<code>fork()</code>の実行に成功したときに<code>active</code>になります。つまり、<code>ExecStart</code>が失敗しようがその前段階の環境変数の設定や<code>ExecStartPre</code>ができていれば<code>active</code>にはなってくれるということです。</p>
<p>一方で明示的に<code>Type</code>を<code>exec</code>に指定した場合、<code>ExecStart</code>で指定したプログラムから<code>execve()</code>の実行が成功したときに<code>active</code>になります。他にも様々な Type ディレクティブがありますが、<code>active</code> になるタイミングがいつなのかはしっかり意識して前処置や後処理を運用していくことが運用上求められるでしょう。</p>
<h2 id="まとめ">まとめ</h2><p>システムを安定させる上で重要なのは、サービスが<code>active</code>になるタイミングやその前後の処理の流れを正確に理解することです。</p>
<p>また、<code>[Unit]</code>セクションに<code>After</code>を記述し、異なるサービス間で起動順序を制御することも一般的です。したがって、これらの順序をより一層意識した運用が求められるでしょう。</p>
]]></content>
    <summary type="html">Linuxのsystemdにおけるservice unitの起動と停止のフローに関して説明します</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="Linux" scheme="https://future-architect.github.io/tags/Linux/"/>
    <category term="入門" scheme="https://future-architect.github.io/tags/%E5%85%A5%E9%96%80/"/>
  </entry>
  <entry>
    <title>全文検索エンジンMeilisearchを試す</title>
    <link href="https://future-architect.github.io/articles/20240411a/"/>
    <id>https://future-architect.github.io/articles/20240411a/</id>
    <published>2024-04-10T15:00:00.000Z</published>
    <updated>2024-04-10T15:00:00.000Z</updated>
    <author><name>岸本卓也</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240411a/meilisearch-logo-light.png" alt="" width="495" height="74">

<h2 id="はじめに">はじめに</h2><p>こんにちは、TIGの岸本卓也です。 春の入門連載2024 の3番目です。</p>
<p>ある静的サイトジェネレーターで生成された膨大なドキュメントの検索において、全文検索機能はあるものの以下の課題を感じることがありました。</p>
<ul>
<li>探したいものがヒットしないことがある</li>
<li>どこがヒットしているのか謎なものが検索結果に含まれることがある</li>
<li>クライアントサイドで動くため、ページ読み込み時に数十MBと大きいことも多いインデックスファイルをダウンロードするため、ページの読み込み完了が遅い原因になっている</li>
</ul>
<p>検索にヒットしない場合は、欲しい情報がありそうなページをリンクから辿って個別に探すしかないのです。しかしこれは手間です。</p>
<p>このような課題を解決すべく新たな全文検索エンジンを探す中でMeilisearchという製品を見つけました。Meilisearchは日本語の検索においても良さそうでかつ手軽に試せたので、試した内容を紹介します。</p>
<h2 id="Meilisearchとは">Meilisearchとは</h2><p>公式サイトの トップページ やドキュメントの 概説ページ によると、検索の応答が早く、すぐに使い始められる、というのが大きな特徴のようです。機能はRESTful APIで提供され、Blogやドキュメントサイトの検索のほか、ECサイトにおける検索への組み込みといったユースケースがあるそうです。</p>
<p>多くの特徴が挙げられています が、中でも以下のように日本語のサポートが明示されているのは安心感があります。</p>
<blockquote>
<p>Comprehensive language support: Optimized support for Chinese, Japanese, Hebrew, and languages using the Latin alphabet<br>包括的な言語サポート: 中国語、日本語、ヘブライ語、およびラテン文字を使用する言語の最適化されたサポート(by Google翻訳)</p>
</blockquote>
<p>日本で活動されている @mosuka (Minoru OSUKA) さんをはじめとしたOSSコミッターの皆さま により日本語処理が改善されているようです。</p>
<h2 id="試用環境">試用環境</h2><p>当記事は以下の環境で実施しました。</p>
<ul>
<li>EC2インスタンス: t3.small</li>
<li>Meilisearchバージョン: v1.7.1 (prototype-japanese-10)</li>
</ul>
<h2 id="セットアップ">セットアップ</h2><p>公式ドキュメントの Installationページ ではインストール方法が複数提示されています。ここでは、日本語向けの公式ビルドバイナリが簡単に使えるDockerイメージの方法で構築します。</p>
<p>日本語向けのビルドバイナリを含むDockerイメージはDocker Hubの こちら で配布されています。Meilisearchのバージョンとイメージタグの対応関係は こちらのPull request に記載されています。</p>
<p>今回は、現時点の最新版であるv1.7.1に対応したDockerイメージを使ってみます。公式の手順通り、pullして…</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">docker pull getmeili/meilisearch:prototype-japanese-10</span><br></pre></td></tr></table></figure>

<p><code>development</code> モードでコンテナを起動します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">docker run -it --<span class="built_in">rm</span> \</span><br><span class="line">    -p 7700:7700 \</span><br><span class="line">    -e MEILI_ENV=<span class="string">&#x27;development&#x27;</span> \</span><br><span class="line">    -e MEILI_MASTER_KEY=<span class="string">&#x27;aSampleMasterKey&#x27;</span> \</span><br><span class="line">    -v $(<span class="built_in">pwd</span>)/meili_data:/meili_data \</span><br><span class="line">    getmeili/meilisearch:prototype-japanese-10</span><br></pre></td></tr></table></figure>

<p>上記で指定しているオプションの内、Meilisearch特有のものは次の目的で指定しています。</p>
<ul>
<li><code>-e MEILI_ENV=&#39;development&#39;</code>: <code>development</code> モードにします。Meilisearchにはインデックスの確認などのために search preview という簡易的なGUIフロントエンドが備わっています。以降の手順でもインデックスの確認にsearch previewを使いますが、 セキュリティ上の理由で <code>development</code> モードでしか使えません。</li>
<li><code>-e MEILI_MASTER_KEY=&#39;aSampleMasterKey&#39;</code>: Master key を指定します。master keyを指定しない場合でも <code>development</code> モードでは自動的にキーが生成されて起動できますが、キーを固定するために指定します。master keyは 16バイト以上の長さが必要 です。</li>
<li><code>-v $(pwd)/meili_data:/meili_data</code>: Dockerコンテナのワーキングディレクトリに作られるデータ を永続化するために指定します。</li>
</ul>
<p>これでMeilisearchが起動しsearch previewも使えるようになっているため、ブラウザで <code>http://&lt;Dockerのホスト&gt;:7700</code> にアクセスしてsearch previewを表示してみます。APIキーの入力を求められるので、コンテナ起動時に指定した <code>aSampleMasterKey</code> を入力して [Go] します。</p>
<img src="/images/2024/20240411a/Meilisearch-search-preview-enter-api-key.png" alt="Meilisearch-search-preview-enter-api-key.png" width="766" height="480" loading="lazy">

<p>まだインデックスを作成していないので何も検索できませんが、これでMeilisearchを使う準備は整いました。</p>
<h2 id="インデックス作成">インデックス作成</h2><p>試しに当ブログサイトのインデックスを作成してみます。</p>
<p>基本的にはAPIを叩いて インデックス に ドキュメント を登録します (cf. ドキュメントを登録するAPIのリファレンス) が、webサイトのスクレイピングツールである docs-scraper が公式に提供されているのでこれを使ってみます。このツールはwebサイトをクローリングしてインデックスを作成してくれるので、設定さえ用意すれば汎用的に使えそうです。なお、docs-scraperのREADMEには…</p>
<blockquote>
<p>🚨 IMPORTANT NOTICE: Reduced Maintenance &amp; Support 🚨</p>
</blockquote>
<p>とあるのでメンテナンスは限定的なようですが、ひとまず使えました。</p>
<h3 id="docs-scraperによるスクレイピング">docs-scraperによるスクレイピング</h3><p>設定ファイルのリファレンスは READMEのこちら に記載があり、設定の具体例は README記載の例 や 公式ドキュメント向けの設定ファイル が参考になります。また、<br>meilisearch&#x2F;docs-scraperは algolia&#x2F;docsearch-scraper のフォークなので、AlgoliaのConfig Filesページ の説明もある程度参考になります。</p>
<p><strong>以降の手順の具体例はそのまま実行はしないようお願いします。</strong> webサイトのクローリングに伴うアクセスは攻撃とみなされる可能性があります (cf. 岡崎市立中央図書館事件)。</p>
<p>当ブログサイトをスクレイピングするのに次のように設定してみました。設定のキモは後述しますが、docs-scraperの大まか動作としては <code>start_urls</code> を起点にこのドメインの範囲内で <code>&lt;a&gt;</code> タグを辿ってクローリングします。</p>
<p><details>
<summary><code>docs-scraper.config.json</code></summary>

<figure class="highlight json"><figcaption><span>docs-scraper.config.json</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;index_uid&quot;</span><span class="punctuation">:</span> <span class="string">&quot;future-tech-blog&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;start_urls&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;https://future-architect.github.io/&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;sitemap_urls&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;https://future-architect.github.io/post-sitemap.xml&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;stop_urls&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;https://future-architect.github.io/categories/&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;https://future-architect.github.io/tags/&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;https://future-architect.github.io/authors/&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;selectors&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;lvl0&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;selector&quot;</span><span class="punctuation">:</span> <span class="string">&quot;.article-category&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;global&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;lvl1&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;selector&quot;</span><span class="punctuation">:</span> <span class="string">&quot;.article-title&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;global&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;lvl2&quot;</span><span class="punctuation">:</span> <span class="string">&quot;main h1&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;lvl3&quot;</span><span class="punctuation">:</span> <span class="string">&quot;main h2&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;lvl4&quot;</span><span class="punctuation">:</span> <span class="string">&quot;main h3&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;lvl5&quot;</span><span class="punctuation">:</span> <span class="string">&quot;main h4&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;lvl6&quot;</span><span class="punctuation">:</span> <span class="string">&quot;main h5&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;text&quot;</span><span class="punctuation">:</span> <span class="string">&quot;main p, main li, main tr, main pre&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;strip_chars&quot;</span><span class="punctuation">:</span> <span class="string">&quot; .,;:#&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;scrap_start_urls&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;custom_settings&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;rankingRules&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;words&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;typo&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;attribute&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;exactness&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;proximity&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;page_rank:desc&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;level:desc&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;position:asc&quot;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;searchableAttributes&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="string">&quot;hierarchy_lvl1&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;hierarchy_lvl2&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;hierarchy_lvl3&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;hierarchy_lvl4&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;hierarchy_lvl5&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;hierarchy_lvl6&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;content&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="string">&quot;hierarchy_lvl0&quot;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;only_content_level&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

</details></p>

<p>docs-scraperも公式からDockerイメージが提供されているので、上記の設定を使ってスクレイピングするには READMEの手順通り 以下で実行できます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">docker run -t --<span class="built_in">rm</span> \</span><br><span class="line">    -e MEILISEARCH_HOST_URL=http://&lt;Dockerのホスト&gt;:7700 \</span><br><span class="line">    -e MEILISEARCH_API_KEY=<span class="string">&#x27;aSampleMasterKey&#x27;</span> \</span><br><span class="line">    -v <span class="string">&quot;<span class="subst">$(pwd)</span>/docs-scraper.config.json&quot;</span>:/docs-scraper/docs-scraper.config.json \</span><br><span class="line">    getmeili/docs-scraper:latest pipenv run ./docs_scraper docs-scraper.config.json</span><br></pre></td></tr></table></figure>

<p>なお、プロキシが必要な場合は適宜 <code>-e HTTP_PROXY=http://example.jp</code> といった形で環境変数を追加する必要があります。</p>
<p>スクレイピングが完了したらsearch previewで確認してみます。Meilisearchセットアップ時に表示したsearch preview画面をリロードすると、スクレイピングで作成したインデックスを選択して検索できるようになりました。</p>
<img src="/images/2024/20240411a/Meilisearch-search-preview-search-demo.gif" alt="Meilisearch-search-preview-search-demo.gif" width="960" height="480" loading="lazy">

<p>docs-scraperによってこのインデックスには97,668個のドキュメントが作られました (フューチャー技術ブログの記事数は現在1,062件です)。search previewでは文字入力の度に検索が走るのですが、今回の環境では各検索は数ミリ秒～数十ミリ秒で応答されるようで、lightning fastという謳い文句に偽りのない軽快さを体感できました。</p>
<p>スクレイピング設定の変更やwebサイトの更新に追従するためなど、インデックスを更新したい場合、上記のdocs-scraper実行を再度行えばよいです。docs-scraperは最初にインデックスを削除&amp;新規作成してからドキュメントを登録していきます。ただ、このようにインデックスを更新するとエンドユーザーに影響があります。インデックスが存在しないタイミングがあったりスクレイピング途中のインデックスが使われてしまうためです。これが問題になる場合、swap indexes APIを使って対策できるようです。Swap indexesは アトミックに処理される そうです。</p>
<p>cf. Swapping indexes<br>cf. Zero downtime index deployment</p>
<p>docs-scraperによって作成される ドキュメント の詳細を確認するため、APIでドキュメントを参照してみます (cf. 単一ドキュメントを取得するAPIのリファレンス)。docs-scraperによって作成されるインデックスではprimary keyとして <code>objectID</code> が設定されています (cf. 単一インデックスの情報を取得するAPIのリファレンス)。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -s \</span></span><br><span class="line"><span class="language-bash">  -X GET <span class="string">&quot;http://127.0.0.1:7700/indexes/future-tech-blog&quot;</span> \</span></span><br><span class="line"><span class="language-bash">  -H <span class="string">&quot;Authorization: Bearer aSampleMasterKey&quot;</span> | jq</span></span><br><span class="line">&#123;</span><br><span class="line">  &quot;uid&quot;: &quot;future-tech-blog&quot;,</span><br><span class="line">  &quot;createdAt&quot;: &quot;2024-04-10T11:07:14.284264598Z&quot;,</span><br><span class="line">  &quot;updatedAt&quot;: &quot;2024-04-10T11:13:42.28654364Z&quot;,</span><br><span class="line">  &quot;primaryKey&quot;: &quot;objectID&quot;</span><br><span class="line">&#125;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -s \</span></span><br><span class="line"><span class="language-bash">  -X GET <span class="string">&quot;http://127.0.0.1:7700/indexes/future-tech-blog/documents/daf5dff8c3dcdce27e0d55e32c8f6d76d99a0eb1&quot;</span> \</span></span><br><span class="line"><span class="language-bash">  -H <span class="string">&quot;Authorization: Bearer aSampleMasterKey&quot;</span> | jq</span></span><br><span class="line">&#123;</span><br><span class="line">  &quot;hierarchy_lvl1&quot;: &quot;Bashのシェル展開&quot;,</span><br><span class="line">  &quot;hierarchy_lvl2&quot;: &quot;プロセス置換 (Process Substitution)&quot;,</span><br><span class="line">  &quot;hierarchy_lvl3&quot;: null,</span><br><span class="line">  &quot;hierarchy_lvl4&quot;: null,</span><br><span class="line">  &quot;hierarchy_lvl5&quot;: null,</span><br><span class="line">  &quot;hierarchy_lvl6&quot;: null,</span><br><span class="line">  &quot;content&quot;: &quot;プロセス置換はプロセスへの入出力をファイルで参照できるようにします。&quot;,</span><br><span class="line">  &quot;hierarchy_lvl0&quot;: &quot;Infrastructureカテゴリ&quot;,</span><br><span class="line">  &quot;anchor&quot;: &quot;プロセス置換-Process-Substitution&quot;,</span><br><span class="line">  &quot;type&quot;: &quot;content&quot;,</span><br><span class="line">  &quot;tags&quot;: [],</span><br><span class="line">  &quot;url&quot;: &quot;https://future-architect.github.io/articles/20210406/#プロセス置換-Process-Substitution&quot;,</span><br><span class="line">  &quot;url_without_variables&quot;: &quot;https://future-architect.github.io/articles/20210406/#プロセス置換-Process-Substitution&quot;,</span><br><span class="line">  &quot;url_without_anchor&quot;: &quot;https://future-architect.github.io/articles/20210406/&quot;,</span><br><span class="line">  &quot;no_variables&quot;: &quot;True&quot;,</span><br><span class="line">  &quot;objectID&quot;: &quot;daf5dff8c3dcdce27e0d55e32c8f6d76d99a0eb1&quot;,</span><br><span class="line">  &quot;page_rank&quot;: 0,</span><br><span class="line">  &quot;level&quot;: 0,</span><br><span class="line">  &quot;position&quot;: 81,</span><br><span class="line">  &quot;hierarchy_radio_lvl0&quot;: null,</span><br><span class="line">  &quot;hierarchy_radio_lvl1&quot;: null,</span><br><span class="line">  &quot;hierarchy_radio_lvl2&quot;: null,</span><br><span class="line">  &quot;hierarchy_radio_lvl3&quot;: null,</span><br><span class="line">  &quot;hierarchy_radio_lvl4&quot;: null,</span><br><span class="line">  &quot;hierarchy_radio_lvl5&quot;: null,</span><br><span class="line">  &quot;hierarchy_radio_lvl6&quot;: null</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>上記ドキュメントのすべての属性 (attribute) はdocs-scraperが独自に定義した属性 (&#x3D;Meilisearchのシステム的な属性はない) です。 Meilisearchで予約された属性が存在する場合、その名前は アンダースコア <code>_</code> 始まりになっています。</p>
<p>webページ本文の情報は <code>hierarchy_lvl0</code> ～ <code>hierarchy_lvl6</code> 及び <code>content</code> 属性に入ります。これらの属性の利用イメージは こちら です。これらの属性値で検索するため、値がうまく入るように設定するのがキモです。これらの属性と設定は以下の通り対応します。</p>
<figure class="highlight text"><table><tr><td class="code"><pre><span class="line">hierarchy_lvl0 ← selectors.lvl0</span><br><span class="line">hierarchy_lvl1 ← selectors.lvl1</span><br><span class="line">hierarchy_lvl2 ← selectors.lvl2</span><br><span class="line">hierarchy_lvl3 ← selectors.lvl3</span><br><span class="line">hierarchy_lvl4 ← selectors.lvl4</span><br><span class="line">hierarchy_lvl5 ← selectors.lvl5</span><br><span class="line">hierarchy_lvl6 ← selectors.lvl6</span><br><span class="line">content ← selectors.text</span><br></pre></td></tr></table></figure>

<p><code>selectors</code> の設定ではどのHTMLタグの内容を取得するかをCSSセレクターで指定します。マークアップ方法はwebサイトごとに異なるので、各webサイトに合ったセレクターに調整する必要があります。CSSセレクターでは取得できない場合、XPathでも指定できます。例えば、以下は <code>class</code> 属性に <code>active</code> と <code>sidebar-link</code> が含まれるタグをXPathで指定する例です。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;selectors&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;lvl0&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;selector&quot;</span><span class="punctuation">:</span> <span class="string">&quot;.//*[(@class and contains(concat(&#x27; &#x27;, normalize-space(@class), &#x27; &#x27;), &#x27; sidebar-link &#x27;)) and (@class and contains(concat(&#x27; &#x27;, normalize-space(@class), &#x27; &#x27;), &#x27; active &#x27;))]&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xpath&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;global&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<h2 id="さいごに">さいごに</h2><p>当記事ではMeilisearchのセットアップと実際のwebサイトのクローリング例を紹介しました。Meilisearchを動かすのも検索の応答速度も謳い文句通りに早くて好印象でした。インデックスを作成するのも、とりあえず動かすだけなら <code>selectors</code> を大まかに設定してみれば良いのでHTML, CSSの知識があれば難易度は低いと思います。</p>
<p>今回は試すに至りませんでしたが、より良い検索結果を得るにはインデックスの作り方や フィルター など使える種々の機能があるようなので、折を見て試していこうと考えています。</p>
<h2 id="参考">参考</h2><ul>
<li>Meilisearch Documentation</li>
</ul>
]]></content>
    <summary type="html">ある静的サイトジェネレーターで生成された膨大なドキュメントの検索において、全文検索機能はあるものの以下の課題を感じることがありました。</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="スクレイピング" scheme="https://future-architect.github.io/tags/%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0/"/>
    <category term="全文検索" scheme="https://future-architect.github.io/tags/%E5%85%A8%E6%96%87%E6%A4%9C%E7%B4%A2/"/>
  </entry>
  <entry>
    <title>ローカルでGoのHTTP/3サーバーを立てて接続テストを行う</title>
    <link href="https://future-architect.github.io/articles/20230927a/"/>
    <id>https://future-architect.github.io/articles/20230927a/</id>
    <published>2023-09-26T15:00:00.000Z</published>
    <updated>2023-09-26T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>Go 1.21ではcrypto&#x2F;tlsパッケージでQUIC関連の更新が少しありましたが、QUICそのものは入りませんでした。QUICとかHTTP&#x2F;3周りをどうするかはいろいろ議論があり、次のことが決定しています。</p>
<ul>
<li>最終的にはnet&#x2F;quicが作られる</li>
<li>ただし、APIの安定化のために、まずは準標準パッケージとして golang.org&#x2F;x&#x2F;net&#x2F;quicを作っていく</li>
<li><code>github.com/quic-go/quic-go</code> という実装はあるが、それをそのまま取り込むことはしない</li>
</ul>
<p>ということで、もう少ししばらくかかりそうです。</p>
<p>といっても、HTTP&#x2F;3のリクエストをエンドのアプリケーションサーバーが直接受けることはおそらく稀で、CDNとか、最前面にたつエンドポイントがHTTP&#x2F;2やHTTP&#x2F;3をしゃべって、その裏はHTTP&#x2F;1.1(非TLS)が多いと思いますし、より固くしようとしてもmTLSでHTTP&#x2F;2じゃないですかね。もともとHTTP&#x2F;3の強みはエラー率の高い回線でも速度が落ちにくいことなので、回線品質の安定したデータセンターとかクラウド内部はHTTP&#x2F;3にしてもうまみがあまりないといえるので、そんな悲観的になることもないかな、と思います。</p>
<p>とはいえ、HTTP&#x2F;3に触ってみたい方もいるかと思うので試してみました。</p>
<p>上記の議論の中でも出てきた <code>github.com/quic-go/quic-go</code> は、このパッケージの作者のMarten SeemannはIETFのQUICワーキンググループの初期からのメンバーでもあり、このパッケージ自身もIETFの他のQUICエージェントとの相互接続テストを行っており、品質はばっちりだと思われます。なのでこれを試してみます。</p>
<h2 id="quic-goでのHTTPサーバー">quic-goでのHTTPサーバー</h2><p>Goの標準ライブラリのサーバー実装と似たようなAPIでサーバーをたてられます。ハンドラ周りはnet&#x2F;httpの<code>http.Handler</code>インターフェースそのものなので、いろんなフレームワークをそのまま上で動かせます。</p>
<figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">   <span class="string">&quot;fmt&quot;</span></span><br><span class="line">   <span class="string">&quot;log&quot;</span></span><br><span class="line">   <span class="string">&quot;net/http&quot;</span></span><br><span class="line"></span><br><span class="line">   <span class="string">&quot;github.com/quic-go/quic-go/http3&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">   mux := http.NewServeMux()</span><br><span class="line">   mux.HandleFunc(<span class="string">&quot;/&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">       fmt.Fprintf(w, <span class="string">&quot;Hello, %s&quot;</span>, r.Proto)</span><br><span class="line">   &#125;)</span><br><span class="line"></span><br><span class="line">   log.Println(<span class="string">&quot;start at https://localhost:8443&quot;</span>)</span><br><span class="line">   log.Println(http3.ListenAndServe(<span class="string">&quot;0.0.0.0:8443&quot;</span>, <span class="string">&quot;localhost.pem&quot;</span>, <span class="string">&quot;localhost-key.pem&quot;</span>, mux))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>Goを書いたことがある人にはおなじみですね？ HTTP&#x2F;2と同じく、TLS必須なので証明書を作成する必要があります。こちらは参考情報がいろいろあるので、お好きな文献などを参考に作ってみてください。Real World HTTPでも紹介しています。</p>
<p><code>localhost</code>のホスト名で証明書を作ったので、<code>localhost:8443</code>でアクセスするとバッチリ表示されます。Safariでアクセスしてみると、すぐに表示されて案外簡単？ と思いつつ、ここから深くなってきます。</p>
<img fetchpriority="high" src="/images/2023/20230927a/スクリーンショット_2023-09-21_15.03.06.png" alt="スクリーンショット_2023-09-21_15.03.06.png" width="806" height="336">

<h2 id="quic-goの内部ではサーバーが2つ起動する">quic-goの内部ではサーバーが2つ起動する</h2><p>http3パッケージには<code>ListenAndServeQUIC()</code>という関数もあります。どう違うのでしょうか？ 最初はこちらで実装してみたのですが動かず、上記の<code>ListenAndServe()</code>にしたら動きました。</p>
<p>こちらの方は内部的には、こんな感じのコードと同じような動きになります。よくよくみると、HTTP&#x2F;2のサーバーも起動していますね。<code>Alt-Svc</code>フィールドを付与しています。</p>
<p>QUICはUDPですが、現在のブラウザはまずTCPでサーバーアクセスしに行きます。しかし、HTTP&#x2F;3のみのサーバーがたっていても、そこにはTCPのサーバーはいません。そのため、HTTP&#x2F;2のサーバーを裏でたてて、そのレスポンスの <code>Alt-Svc</code>フィールドを返し「こちらでHTTP&#x2F;3のサーバーがいるよ」とブラウザをHTTP&#x2F;3の方に誘導しているというわけです。</p>
<figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">   <span class="string">&quot;context&quot;</span></span><br><span class="line">   <span class="string">&quot;fmt&quot;</span></span><br><span class="line">   <span class="string">&quot;log&quot;</span></span><br><span class="line">   <span class="string">&quot;net/http&quot;</span></span><br><span class="line">   <span class="string">&quot;os&quot;</span></span><br><span class="line">   <span class="string">&quot;os/signal&quot;</span></span><br><span class="line">   <span class="string">&quot;sync&quot;</span></span><br><span class="line"></span><br><span class="line">   <span class="string">&quot;github.com/quic-go/quic-go/http3&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">   mux := http.NewServeMux()</span><br><span class="line">   mux.HandleFunc(<span class="string">&quot;/&quot;</span>, <span class="function"><span class="keyword">func</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">       w.Header().Add(<span class="string">&quot;Alt-Svc&quot;</span>, <span class="string">`h3=&quot;:8443&quot;; ma=2592000`</span>)</span><br><span class="line">       fmt.Fprintf(w, <span class="string">&quot;Hello via %s&quot;</span>, r.Proto)</span><br><span class="line">   &#125;)</span><br><span class="line"></span><br><span class="line">   ctx, <span class="built_in">close</span> := signal.NotifyContext(context.Background(), os.Interrupt)</span><br><span class="line">   <span class="keyword">defer</span> <span class="built_in">close</span>()</span><br><span class="line"></span><br><span class="line">   h2server := &amp;http.Server&#123;</span><br><span class="line">       Addr:    <span class="string">&quot;0.0.0.0:8443&quot;</span>,</span><br><span class="line">       Handler: mux,</span><br><span class="line">   &#125;</span><br><span class="line">   h3server := &amp;http3.Server&#123;</span><br><span class="line">       Addr:    <span class="string">&quot;0.0.0.0:8443&quot;</span>,</span><br><span class="line">      Handler: mux,</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">   wg := &amp;sync.WaitGroup&#123;&#125;</span><br><span class="line">   wg.Add(<span class="number">2</span>)</span><br><span class="line"></span><br><span class="line">   <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">       log.Println(<span class="string">&quot;start at http/2 server at (TCP)https://localhost:8443&quot;</span>)</span><br><span class="line">       log.Println(h2server.ListenAndServeTLS(<span class="string">&quot;localhost.pem&quot;</span>, <span class="string">&quot;localhost-key.pem&quot;</span>))</span><br><span class="line">       wg.Done()</span><br><span class="line">   &#125;()</span><br><span class="line"></span><br><span class="line">   <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">       log.Println(<span class="string">&quot;start at http/3 server at (UDP)https://localhost:8443&quot;</span>)</span><br><span class="line">       log.Println(h3server.ListenAndServeTLS(<span class="string">&quot;localhost.pem&quot;</span>, <span class="string">&quot;localhost-key.pem&quot;</span>))</span><br><span class="line">       wg.Done()</span><br><span class="line">   &#125;()</span><br><span class="line"></span><br><span class="line">   &lt;-ctx.Done()</span><br><span class="line">   h2server.Shutdown(ctx)</span><br><span class="line">   h3server.Close()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="プロトコル選択">プロトコル選択</h2><p>HTTP&#x2F;3ではいくつかの箇所でプロトコル選択をさせるようになっています。</p>
<ul>
<li>DNSのHTTPSレコード</li>
<li>TLSのハンドシェイクのALPN(Application-Layer Protocol Negotiation)</li>
<li>上記で紹介したAlt-Svcフィールド</li>
</ul>
<p>上から順番に実行されます。DNSであれば、最初のサーバーアクセスの前に情報を得ることができます。TLSのハンドシェイクはTCP&#x2F;UDPを決めた後に行えるため、最初のTCPのアクセスの空撃ちは必要です。Alt-Svcフィールドのタイミングはさらに遅く、一度HTTP&#x2F;2のサーバーがリクエストを処理するまで、ブラウザはHTTP&#x2F;3のサーバーの存在を知ることはできません。</p>
<p>個人的には103 Early HintsでAlt-Svcフィールドを返せば僅かばかりAlt-Svcをレスポンスで返すよりも効率的かな、と思いましたが、103 Early Hintsはサンプル見てもプリロードの用途しか見ませんね。</p>
<h2 id="CoreDNSをたててみた">CoreDNSをたててみた</h2><p>さて、上記のHTTP&#x2F;3サーバーですが、Safariからはばっちり3の方につながるのですが、ChromeとFirefoxは2ばかり。たまに開発者ツールを開いてリロードしたタイミングだけ3でつながったりといまいち安定しません。そんな中、Twitter(X)で、まえかわさんという方から、HTTPSレコードでばっちりいけるというお話を伺い、ちょっと試してみることにしました。</p>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">allt-svcに加えてHTTPSレコードをつけたらchromeは確実に3でfirefoxは半々ぐらいの確率になりますね<br>自分も壊れてるのかと思いました</p>&mdash; まえかわ (@kiwithe2027) September 17, 2023</blockquote> 

<p>ちなみに、現在出ているSoftware Design 2023年10月号がたまたまHTTP&#x2F;3特集でしたが、HTTPSレコードはCDN上の設定で付与していました。</p>
<p>今回あつかったCoreDNSは、CNCFの傘下にいるプロジェクトで、Kubernetesでもよく使われています。もっとも、その理由がetcdでエントリーの管理ができて、そちらの情報を元に情報を返せるため管理が楽、というところがあるのだと思いますが、今回はetcdは使いません。</p>
<p>こちらをみながらmacOSでやってみた例になりますが、サービスの起動部分以外はポータブルなはずです。hnakamurさんがWindowsでサービス化するラッパーを作られているのでこちらに変えればWindowsでも動くかと思います。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">インストール</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">brew install coredns</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">起動</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> brew services start coredns</span></span><br></pre></td></tr></table></figure>

<p><code>http3.test</code>というホスト名だけ特別扱いしてHTTPSレコードをつけてあげたいので、<code>/opt/homebrew/etc/coredns/Corefile</code>ファイルを次のようにしました。</p>
<figure class="highlight sh"><figcaption><span>/opt/homebrew/etc/coredns/Corefile</span></figcaption><table><tr><td class="code"><pre><span class="line">. &#123;</span><br><span class="line">    forward . 8.8.8.8</span><br><span class="line">    <span class="built_in">log</span></span><br><span class="line">    errors</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">http3.test &#123;</span><br><span class="line">  file /Users/shibu/.config/coredns/test</span><br><span class="line">  <span class="built_in">log</span></span><br><span class="line">  errors</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>上記の設定からインポートされる自分のホーム以下に追加の設定ファイル(&#x2F;Users&#x2F;shibu&#x2F;.config&#x2F;coredns&#x2F;test)をおきます。こんな感じにしてみました。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="variable">$ORIGIN</span> <span class="built_in">test</span>.</span><br><span class="line"><span class="variable">$TTL</span> 1m</span><br><span class="line"></span><br><span class="line">@                 IN  SOA     ns.test. admin.test. (</span><br><span class="line">                                   2020010510     ; Serial</span><br><span class="line">                                   1m             ; Refresh</span><br><span class="line">                                   2m             ; Retry</span><br><span class="line">                                   4m             ; Expire</span><br><span class="line">                                   1m)            ; Minimum TTL</span><br><span class="line">@                 IN  A       127.0.0.1</span><br><span class="line">@                 IN  NS      <span class="built_in">test</span>.</span><br><span class="line">ns                IN  CNAME   @</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">http3   IN A     	127.0.0.1</span><br><span class="line">        IN AAAA  	::1</span><br><span class="line">        IN		HTTPS 1 . alpn=h3 port=8443</span><br></pre></td></tr></table></figure>

<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">再起動</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">sudo</span> brew services restart coredns</span></span><br></pre></td></tr></table></figure>

<p>最後にネットワーク設定でDNSに127.0.0.1を追加してあげれば完了です。</p>
<p>これでSafariでアクセスしてみると、無事に <code>https://http3.test</code>というURLでアクセスできました。HTTPSレコードはプロトコル以外にもポートを設定できるのでポート番号を省略できるようになります。いいですね。</p>
<img src="/images/2023/20230927a/スクリーンショット_2023-09-21_15.02.30.png" alt="" width="699" height="336" loading="lazy">

<p>しかし、実はChromeとFirefox、EdgeはこのDNSサーバーを見に行ってくれませんでした。昔はDNS設定があったと思いますが、いまはDNS over HTTPSのみです。ChromeでHTTP&#x2F;3に繋ぎたくてCoreDNSを入れてみたのですが、ここはうまく行っていません。DNS over HTTPSをCoreDNSで建ててみてもそこにアクセスしてくれなかったり、設定を拒否されたり。ここはぼちぼちやっていこうと思います。dnsmasqを入れた時はアクセスはしてくれたものの、HTTPSレコードの追加が分からずCoreDNSでやりましたが、別の方法も試そうかと。</p>
<h2 id="まとめ">まとめ</h2><p>というわけで、HTTP&#x2F;3のサーバーを起動してブラウザでアクセスしてみました。Safariからはうまくつながりました。しかし、実際に試す時間の90％はDNS周りを操作する時間だったりして、ちょっと敷居が高い気がしました。</p>
<p>将来的にはもう開発体験がちょっと改善されたらいいな、と思いました。</p>
]]></content>
    <summary type="html">Go 1.21ではcrypto/tlsパッケージでQUIC関連の更新が少しありましたが、QUICそのものは入りませんでした。QUICとかHTTP/3周りをどうするかはいろいろ議論があり...</summary>
    <category term="Infrastructure" scheme="https://future-architect.github.io/categories/Infrastructure/"/>
    <category term="DNS" scheme="https://future-architect.github.io/tags/DNS/"/>
    <category term="Go" scheme="https://future-architect.github.io/tags/Go/"/>
    <category term="Go1.21" scheme="https://future-architect.github.io/tags/Go1-21/"/>
    <category term="HTTP" scheme="https://future-architect.github.io/tags/HTTP/"/>
  </entry>
</feed>
