<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title><![CDATA[Gosuke Miyashita]]></title>
    <link href="https://mizzy.org/atom.xml" rel="self"/>
    <link href="https://mizzy.org/"/>
    <updated>2026-05-17T18:08:30+09:00</updated>
    <id>https://mizzy.org/</id>
    <author>
        <name><![CDATA[Gosuke Miyashita]]></name>
    </author>
    <generator uri="https://github.com/mizzy/nebel/">Nebel</generator>

    <entry>
        <title type="html"><![CDATA[Carinaのwait構文]]></title>
        <link href="https://mizzy.org/blog/2026/05/17/1/"/>
        <updated>2026-05-17T18:08:30+09:00</updated>
        <id>https://mizzy.org/blog/2026/05/17/1/</id>
        <content type="html"><![CDATA[
<p><a href="https://github.com/carina-rs/carina">Carina</a>に<code>wait</code>という構文を入れた。「あるリソースが特定の状態になるまで、それに依存するリソースの作成をブロックする」ための仕組みで、Terraformでいうと<code>aws_acm_certificate_validation</code>のような「待ち合わせ専用リソース」に相当する。</p>
<p>現在構築中のCarina provider registry（Carina自身で管理している）でCloudFront + ACMの構成を組んでいて、ACM証明書がISSUEDになるまでCloudFrontの作成を待ちたい、というのが直接のきっかけ。Terraform流の「待ち合わせリソース」をproviderに足すアプローチも検討したけど、結局DSL側に<code>wait</code>構文を入れる方向にした。その経緯と設計について書いておく。</p>
<hr />
<h2>ACM + CloudFrontでの待ち合わせ問題</h2>
<p>ACMでDNS検証を使ってCloudFront用の証明書を発行する場合、流れはこうなる。</p>
<ol>
<li>ACM証明書をリクエストする</li>
<li>証明書のレスポンスに含まれる検証情報から、検証用のCNAMEレコードの内容を取得する</li>
<li>Route 53にその検証用CNAMEレコードを作る</li>
<li>ACMがそのレコードを観測して証明書のステータスを<code>PENDING_VALIDATION</code> → <code>ISSUED</code>に遷移させる</li>
<li>CloudFrontなど下流のリソースが<code>ISSUED</code>になった証明書のARNを使い始める</li>
</ol>
<p>問題は4と5の間で、証明書がまだ<code>ISSUED</code>になっていないうちにCloudFrontの作成が走ってしまうケース。CloudFrontはviewer certificateに<code>ISSUED</code>済みの証明書しか受け付けないので、<code>PENDING_VALIDATION</code>のままの証明書を渡すと作成自体が失敗する。なので「証明書が<code>ISSUED</code>になるまでCloudFrontの作成を止める」という同期点が必要になる。</p>
<p>Terraformでは<code>aws_acm_certificate_validation</code>という専用のリソースを介してこれを実現している。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_acm_certificate&#34; &#34;cert&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  domain_name</span>       <span class="o">=</span> <span class="s2">&#34;registry.carina-rs.dev&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  validation_method</span> <span class="o">=</span> <span class="s2">&#34;DNS&#34;</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_route53_record&#34; &#34;validation&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  zone_id</span> <span class="o">=</span> <span class="k">aws_route53_zone</span><span class="p">.</span><span class="k">zone</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="cl"><span class="n">  name</span>    <span class="o">=</span> <span class="k">tolist</span><span class="p">(</span><span class="k">aws_acm_certificate</span><span class="p">.</span><span class="k">cert</span><span class="p">.</span><span class="k">domain_validation_options</span><span class="p">)[</span><span class="m">0</span><span class="p">].</span><span class="k">resource_record_name</span>
</span></span><span class="line"><span class="cl"><span class="n">  type</span>    <span class="o">=</span> <span class="k">tolist</span><span class="p">(</span><span class="k">aws_acm_certificate</span><span class="p">.</span><span class="k">cert</span><span class="p">.</span><span class="k">domain_validation_options</span><span class="p">)[</span><span class="m">0</span><span class="p">].</span><span class="k">resource_record_type</span>
</span></span><span class="line"><span class="cl"><span class="n">  records</span> <span class="o">=</span> <span class="p">[</span><span class="k">tolist</span><span class="p">(</span><span class="k">aws_acm_certificate</span><span class="p">.</span><span class="k">cert</span><span class="p">.</span><span class="k">domain_validation_options</span><span class="p">)[</span><span class="m">0</span><span class="p">].</span><span class="k">resource_record_value</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">  ttl</span>     <span class="o">=</span> <span class="m">60</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_acm_certificate_validation&#34; &#34;cert&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  certificate_arn</span>         <span class="o">=</span> <span class="k">aws_acm_certificate</span><span class="p">.</span><span class="k">cert</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="cl"><span class="n">  validation_record_fqdns</span> <span class="o">=</span> <span class="p">[</span><span class="k">aws_route53_record</span><span class="p">.</span><span class="k">validation</span><span class="p">.</span><span class="k">fqdn</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_cloudfront_distribution&#34; &#34;dist&#34;</span> {<span class="c1">
</span></span></span><span class="line"><span class="cl"><span class="c1">  # ...
</span></span></span><span class="line"><span class="cl">  <span class="k">viewer_certificate</span> {
</span></span><span class="line"><span class="cl"><span class="n">    acm_certificate_arn</span> <span class="o">=</span> <span class="k">aws_acm_certificate_validation</span><span class="p">.</span><span class="k">cert</span><span class="p">.</span><span class="k">certificate_arn</span><span class="c1">
</span></span></span><span class="line"><span class="cl"><span class="c1">    # 直接 aws_acm_certificate.cert.arn を参照すると ISSUED 前に作成が走る
</span></span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre><p>下流のCloudFrontは<code>aws_acm_certificate.cert.arn</code>ではなく、<code>aws_acm_certificate_validation.cert.certificate_arn</code>を参照する。<code>aws_acm_certificate_validation</code>が「ISSUEDになるまでブロックする」という役割を担っている。</p>
<p>ただ、この<code>aws_acm_certificate_validation</code>という設計には少し違和感がある。</p>
<ul>
<li>AWS側に対応する実リソースが無い。<code>aws_acm_certificate_validation</code>という名前のものがAWSに作られるわけではなく、実体は「証明書がISSUEDになるまで待つ」という処理でしかない。「リソース」と書いてあるのに実体がないので、初見だと何をしているリソースなのか分かりにくい</li>
<li>同じパターンが他のサービスでも繰り返される。RDSの<code>available</code>待ち、EC2の<code>running</code>待ち、Lambdaの<code>Active</code>待ち、など、各providerに似たような「待ち合わせ専用リソース」が並ぶことになる</li>
</ul>
<p>これは<code>aws_acm_certificate_validation</code>に限った話ではなく、Terraformでは「実体のない繋ぎ・待ち合わせ用のリソース」というパターンが広く使われている。<code>time_sleep</code>（指定秒数待つ）、<code>null_resource</code>、<code>terraform_data</code>あたりが代表例で、いずれもAWS等に対応する実体は無く、依存関係のエッジを足したり処理をフックしたりするためだけに「リソース」の形を借りている。</p>
<p>これ自体は実害があるというより、使う側として「これは何のリソースなんだろう」と一瞬考えさせられる、という類の引っかかり。Terraformのリソースモデルが表現力豊かなので、本来リソースでないものまでリソースの形で書けてしまう、とも言える。</p>
<p>Carinaでも同じく専用リソース (<code>aws.acm.CertificateValidation</code>) を足す手もあったけど、実体のない待ち合わせ処理をリソースとして表現するのは、やはり違和感がある。Terraformと同じ妥協を後発のCarinaでわざわざ持ち込まなくてもいいのでは、と考えて、「待ち合わせ」をDSLの第一級構文として導入することにした。設計の詳細は<a href="https://github.com/carina-rs/carina/blob/main/notes/specs/2026-05-09-wait-construct-design.md">notes/specs/2026-05-09-wait-construct-design.md</a>に書いてある。</p>
<hr />
<h2>Carinaの<code>wait</code>構文</h2>
<p>実際の構文はこんな感じ。実際に運用しているACM証明書の定義から抜粋。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="k">let</span> <span class="na">cert</span> <span class="o">=</span> <span class="nc">aws.acm.Certificate</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">domain_name</span>       <span class="o">=</span> <span class="nv">domain_name</span>
</span></span><span class="line"><span class="cl">  <span class="na">validation_method</span> <span class="o">=</span> <span class="nv">dns</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">let</span> <span class="na">validation_record</span> <span class="o">=</span> <span class="nc">aws.route53.RecordSet</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">hosted_zone_id</span>   <span class="o">=</span> <span class="nv">zone</span><span class="p">.</span><span class="nv">id</span>
</span></span><span class="line"><span class="cl">  <span class="na">name</span>             <span class="o">=</span> <span class="nv">cert</span><span class="p">.</span><span class="nv">domain_validation_options</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nv">resource_record</span><span class="p">.</span><span class="nv">name</span>
</span></span><span class="line"><span class="cl">  <span class="na">type</span>             <span class="o">=</span> <span class="nv">cname</span>
</span></span><span class="line"><span class="cl">  <span class="na">ttl</span>              <span class="o">=</span> <span class="mi">300</span>
</span></span><span class="line"><span class="cl">  <span class="na">resource_records</span> <span class="o">=</span> <span class="p">[</span><span class="nv">cert</span><span class="p">.</span><span class="nv">domain_validation_options</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nv">resource_record</span><span class="p">.</span><span class="nv">value</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">let</span> <span class="na">cert_issued</span> <span class="o">=</span> <span class="nv">wait</span> <span class="nv">cert</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">until</span>      <span class="o">=</span> <span class="nv">cert</span><span class="p">.</span><span class="nv">status</span> <span class="o">==</span> <span class="nc">aws.acm.Certificate.Status.issued</span>
</span></span><span class="line"><span class="cl">  <span class="na">depends_on</span> <span class="o">=</span> <span class="p">[</span><span class="nv">validation_record</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="na">timeout</span>    <span class="o">=</span> <span class="err">75</span><span class="nv">min</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><p><code>let &lt;name&gt; = wait &lt;target&gt; { ... }</code>という形で、ターゲットとなるリソースのbinding名 (<code>cert</code>) を<code>wait</code>の直後に置く。ブロックの中には待ち合わせの挙動を指定する属性を書く。</p>
<ul>
<li><code>until</code> — <code>read()</code>の結果がこの述語を満たすまで待つ</li>
<li><code>depends_on</code> — pollを始める前に完了させておきたいbinding。任意。指定しなくても<code>until</code>が満たされるまで待つので最終結果は変わらないが、ACMの場合、検証レコードが作られるまではどう頑張ってもISSUEDにならない。その間pollし続けても無駄だし、<code>timeout</code>のカウントも進んでしまう。検証レコードの作成完了を待ってからpollを始めるよう、<code>depends_on = [validation_record]</code>を指定しておく</li>
<li><code>timeout</code> — 待ち時間の上限。省略するとリソースのスキーマで宣言されたデフォルトが使われる。ACM Certificateは75分にしてある。これは特に深い根拠があるわけではなく、Terraformの<code>aws_acm_certificate_validation</code>のデフォルトタイムアウトが75分なので、それに揃えただけ</li>
</ul>
<p>下流のCloudFrontは<code>cert.certificate_arn</code>ではなく<code>cert_issued.certificate_arn</code>を参照する。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="k">let</span> <span class="na">distribution</span> <span class="o">=</span> <span class="nc">awscc.cloudfront.Distribution</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">distribution_config</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c"># ...</span>
</span></span><span class="line"><span class="cl">    <span class="na">viewer_certificate</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="na">acm_certificate_arn</span>      <span class="o">=</span> <span class="nv">cert_issued</span><span class="p">.</span><span class="nv">certificate_arn</span>
</span></span><span class="line"><span class="cl">      <span class="na">ssl_support_method</span>       <span class="o">=</span> <span class="nv">sni_only</span>
</span></span><span class="line"><span class="cl">      <span class="na">minimum_protocol_version</span> <span class="o">=</span> <span class="nv">tlsv1_2_2021</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><p><code>cert_issued</code>は値としては<code>cert</code>のpassthroughで、<code>cert_issued.certificate_arn</code>は<code>cert.certificate_arn</code>と同じ文字列を返す。違うのは依存関係のエッジだけで、<code>cert_issued</code>を参照したリソースは「<code>cert</code>が<code>ISSUED</code>になるまでブロックされる」という同期契約を継承する。</p>
<hr />
<h2>providerには手を入れない</h2>
<p><code>wait</code>の実装はすべてcarina-core側（executor）で行う。providerには手を入れていない。</p>
<p>executorは<code>wait</code>エフェクトを以下の手順で処理する。</p>
<ol>
<li><code>depends_on</code>に指定されたエフェクト（ターゲットの<code>Create</code>/<code>Update</code>、ユーザー指定の追加依存）が完了するまで待つ</li>
<li><code>provider.read(target_id, ...)</code>を呼ぶ</li>
<li>返ってきた<code>State.attributes</code>に対して<code>until</code>述語を評価する</li>
<li>真なら成功。そのときの<code>State</code>スナップショットを下流参照用にキャプチャする</li>
<li>偽なら、経過時間が<code>timeout</code>を超えていればタイムアウトエラー、超えていなければ<code>interval</code>だけ<code>sleep</code>して2に戻る</li>
</ol>
<p>providerに対して呼ばれるのは普通の<code>read()</code>だけで、provider側は<code>wait</code>の存在を知らない。Terraformのように待ち合わせ専用のリソースをproviderごとに実装しなくても、どのproviderのどのリソースでも<code>wait</code>できる、というのが地味にうれしいポイント。</p>
<hr />
<h2>まとめ</h2>
<p>Terraformの<code>aws_acm_certificate_validation</code>とCarinaの<code>wait</code>の違いを並べるとこんな感じ。</p>
<table>
<thead>
<tr>
<th>Terraformの<code>aws_acm_certificate_validation</code></th>
<th>Carinaの<code>wait</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>providerごとに専用リソース (<code>~Validation</code>、<code>~Ready</code> など) を足す必要がある</td>
<td>DSLの第一級構文。どのproviderのどのリソースにも使える</td>
</tr>
<tr>
<td><code>aws_acm_certificate_validation</code>の作成処理が同期するため、providerの責務にwait処理が入る</td>
<td>wait処理はexecutor側にあり、providerはwaitしない</td>
</tr>
</tbody>
</table>
<p>ACM以外にも、EC2の<code>running</code>待ち、RDSの<code>available</code>待ち、Lambdaの<code>Active</code>待ち、など同じパターンが他のサービスでも使える。</p>

]]></content>
    </entry>

    <entry>
        <title type="html"><![CDATA[Carinaのupstream state機能]]></title>
        <link href="https://mizzy.org/blog/2026/05/04/1/"/>
        <updated>2026-05-04T08:47:03+09:00</updated>
        <id>https://mizzy.org/blog/2026/05/04/1/</id>
        <content type="html"><![CDATA[
<p><a href="https://github.com/carina-rs/carina">Carina</a>に<code>upstream_state</code>という機能を入れた。別のCarinaコンポーネントのstateを参照するための仕組みで、Terraformでいう<code>terraform_remote_state</code>に相当する。</p>
<p>ただ、Terraformのremote stateを使っていて気になる点があったので、Carinaでは少し違う設計にしてみた。その違いについて書いておく。</p>
<hr />
<h2>Terraformのremote stateのおさらい</h2>
<p>Terraformで別コンポーネントのstateを参照するときは、<code>terraform_remote_state</code> data sourceを使う。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;terraform_remote_state&#34; &#34;network&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  backend</span> <span class="o">=</span> <span class="s2">&#34;s3&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  config</span> <span class="o">=</span> {
</span></span><span class="line"><span class="cl"><span class="n">    bucket</span> <span class="o">=</span> <span class="s2">&#34;my-tfstate&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    key</span>    <span class="o">=</span> <span class="s2">&#34;network/terraform.tfstate&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    region</span> <span class="o">=</span> <span class="s2">&#34;ap-northeast-1&#34;</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_security_group&#34; &#34;web&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  vpc_id</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">terraform_remote_state</span><span class="p">.</span><span class="k">network</span><span class="p">.</span><span class="k">outputs</span><span class="p">.</span><span class="k">vpc_id</span>
</span></span><span class="line"><span class="cl">}
</span></span></code></pre><p>参照元のoutputsを<code>data.terraform_remote_state.network.outputs.vpc_id</code>のような形で読む。参照される側は<code>output</code>ブロックで値を公開する。</p>
<p>使っていて気になる点がいくつかある。</p>
<h3>参照する側がbackendの設定を知っている必要がある</h3>
<p>networkコンポーネントのbackendがS3なのか、バケット名は何か、キーは何か、といった情報を、それを使うweb側のコードにも書く必要がある。参照元のnetwork側ではこう書いて、</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="c1"># network/main.tf
</span></span></span><span class="line"><span class="cl"><span class="k">terraform</span> {
</span></span><span class="line"><span class="cl">  <span class="k">backend</span> <span class="s2">&#34;s3&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">    bucket</span> <span class="o">=</span> <span class="s2">&#34;my-tfstate&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    key</span>    <span class="o">=</span> <span class="s2">&#34;network/terraform.tfstate&#34;</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre><p>参照する側のweb側ではこう書く。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="c1"># web/main.tf
</span></span></span><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;terraform_remote_state&#34; &#34;network&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  backend</span> <span class="o">=</span> <span class="s2">&#34;s3&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  config</span> <span class="o">=</span> {
</span></span><span class="line"><span class="cl"><span class="n">    bucket</span> <span class="o">=</span> <span class="s2">&#34;my-tfstate&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    key</span>    <span class="o">=</span> <span class="s2">&#34;network/terraform.tfstate&#34;</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre><p>bucketとkeyが両方に登場しているのが分かる。実質的に同じ情報を二重管理している状態になる。</p>
<h3>呼び出すのに長い文字列を書く必要がある</h3>
<p>参照するときの記述が<code>data.terraform_remote_state.network.outputs.vpc_id</code>となって、なかなか長い。<code>data.terraform_remote_state.</code>と<code>.outputs.</code>が毎回ついてくる。</p>
<p>複数の値を参照していると、この長い名前があちこちに出てきて冗長になる。localsで別名をつけて短くする、みたいな回避策を取ることになる。</p>
<h3>補完が効かない</h3>
<p><code>outputs</code>の中身は実際のstateを読まないと分からないので、エディタで補完が効かない。</p>
<p>補完が効かないので、typoしても実行するまで気付けない。<code>terraform validate</code>は実際のremote stateを読まずに構文チェックだけするので、ここでは検出されない。別コンポーネントで<code>output</code>の名前を変えたときに、参照側が壊れていることを<code>plan</code>や<code>apply</code>のタイミングで初めて知ることになる。</p>
<h3>参照元が未applyだとplanできない</h3>
<p>共通の通知基盤となるSNS topicをmonitoringコンポーネントで管理し、各サービス側のCloudWatchアラームからそのtopicを参照する、という構成を考える。</p>
<p>monitoring側に新しい通知先（たとえば<code>critical_alerts_topic_arn</code>）を追加し、同じPRでweb側の新しいアラームからそれを参照する、というPRを出したとする。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="c1"># monitoring/main.tf
</span></span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_sns_topic&#34; &#34;critical_alerts&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;critical-alerts&#34;</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">output</span> <span class="s2">&#34;critical_alerts_topic_arn&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  value</span> <span class="o">=</span> <span class="k">aws_sns_topic</span><span class="p">.</span><span class="k">critical_alerts</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="cl">}
</span></span></code></pre><pre class="chroma"><code><span class="line"><span class="cl"><span class="c1"># web/main.tf
</span></span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_metric_alarm&#34; &#34;web_5xx&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  alarm_name</span>  <span class="o">=</span> <span class="s2">&#34;web-5xx&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  metric_name</span> <span class="o">=</span> <span class="s2">&#34;5XXError&#34;</span><span class="c1">
</span></span></span><span class="line"><span class="cl"><span class="c1">  # ...
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">  alarm_actions</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="k">data</span><span class="p">.</span><span class="k">terraform_remote_state</span><span class="p">.</span><span class="k">monitoring</span><span class="p">.</span><span class="k">outputs</span><span class="p">.</span><span class="k">critical_alerts_topic_arn</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">]</span>
</span></span><span class="line"><span class="cl">}
</span></span></code></pre><p>このPRをCIで<code>plan</code>にかけると、monitoring側がまだ<code>apply</code>されていないので新しいoutputがstateに存在せず、web側の<code>plan</code>はエラーになる。</p>
<p>planエラーを回避したい場合、以下のように<code>try()</code>でフォールバックする、という手もある。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="n">alarm_actions</span> <span class="o">=</span> <span class="k">try</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="p">[</span><span class="k">data</span><span class="p">.</span><span class="k">terraform_remote_state</span><span class="p">.</span><span class="k">monitoring</span><span class="p">.</span><span class="k">outputs</span><span class="p">.</span><span class="k">critical_alerts_topic_arn</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="p">[],</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre><p>ただ、これだとPRレビュー時に見えるplanは<code>alarm_actions = []</code>になり、monitoringを<code>apply</code>した後の本来のplanとは異なる。</p>
<p>結局、monitoring側のPRとweb側のPRを2回に分けてマージする、という対応になりがち。</p>
<p>そもそもremote stateで参照するほど密に依存しているなら、コンポーネントを分割している設計自体を見直したほうがいいケースもある。ただ、stateを分けざるを得ない事情（チーム境界、applyの責任範囲、リソースへのアクセス権限を分けたい都合など）もあり、そういう場合はこの面倒さと付き合うしかない。</p>
<p>remote stateの代わりに、data sourceで名前やタグから引く、という方法もある。これだとremote stateのように「outputが追加されたか」は気にしなくていい。ただし結局、参照先のリソースが実在していないとdata sourceの読み込みに失敗するので、upstreamを先に<code>apply</code>する必要があるのは変わらない。</p>
<p>さらにdata sourceはstateではなく実際のAWS APIを叩いてリソース情報を取得するので、参照が増えるとstate refreshが遅くなる。どちらを取るかは悩ましい。</p>
<hr />
<h2>Carinaのupstream_state</h2>
<p>Carinaでは、参照する側はupstreamコンポーネントの<strong>ディレクトリを指すだけ</strong>にした。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="k">let</span> <span class="na">network</span> <span class="o">=</span> <span class="k">upstream_state</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">source</span> <span class="o">=</span> <span class="s1">&#39;../network&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nc">awscc.ec2.SecurityGroup</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">group_description</span> <span class="o">=</span> <span class="s1">&#39;Web security group&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="na">vpc_id</span>            <span class="o">=</span> <span class="nv">network</span><span class="p">.</span><span class="nv">vpc_id</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="na">tags</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="na">Name</span> <span class="o">=</span> <span class="s1">&#39;web-sg&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><p><code>source</code>はupstreamコンポーネントのディレクトリで、<code>.crn</code>ファイルからの相対パスで解決される。<code>let</code>で束縛した名前（この例では<code>network</code>）を通じて、upstream側が公開している値にアクセスできる。</p>
<p>ディレクトリ参照なので、別リポジトリにあるコンポーネントを参照するにはsubtreeで取り込むなどの工夫がいる。自分にはリポジトリをまたいでstateを参照したいニーズがないので、いまはこれで割り切っている。必要になったらそのとき考える。</p>
<p>公開する側はこんな感じ。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="k">backend</span> <span class="nv">s3</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">bucket</span> <span class="o">=</span> <span class="s1">&#39;my-carina-state&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="na">key</span>    <span class="o">=</span> <span class="s1">&#39;network/carina.state.json&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">let</span> <span class="na">main</span> <span class="o">=</span> <span class="nc">awscc.ec2.Vpc</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">cidr_block</span> <span class="o">=</span> <span class="s1">&#39;10.0.0.0/16&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">exports</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">vpc_id</span> <span class="o">=</span> <span class="nv">main</span><span class="p">.</span><span class="nv">vpc_id</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><p><code>exports</code>ブロックで公開したい値を宣言する。Terraformの<code>output</code>ブロックに相当する。型は自動的に推論されるので、この例では<code>vpc_id</code>は<code>VpcId</code>型として公開される。</p>
<hr />
<h2>Terraformと違うところ</h2>
<p>先に挙げた4つの点が、それぞれ<code>upstream_state</code>ではどうなっているかを書いていく。</p>
<h3>backend設定を書かなくていい</h3>
<p>参照する側に書くのは<code>source</code>（ディレクトリ）だけ。backend設定は書かない。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="c"># web/main.crn</span>
</span></span><span class="line"><span class="cl"><span class="k">let</span> <span class="na">network</span> <span class="o">=</span> <span class="k">upstream_state</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">source</span> <span class="o">=</span> <span class="s1">&#39;../network&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>Carinaは<code>source</code>で指定されたディレクトリの<code>.crn</code>ファイルを読み込み、そこに書かれている<code>backend</code>ブロックを解決し、stateを読み出す。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="c"># network/main.crn</span>
</span></span><span class="line"><span class="cl"><span class="k">backend</span> <span class="nv">s3</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">bucket</span> <span class="o">=</span> <span class="s1">&#39;my-carina-state&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="na">key</span>    <span class="o">=</span> <span class="s1">&#39;network/carina.state.json&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>つまり、backendの場所を知っているのはupstream側だけで、参照する側はそれを意識しなくていい。</p>
<p>ディレクトリ参照なので、LSPでパス補完が効く。さらに、存在しないパスを指定した場合は<code>carina validate</code>の時点でエラーになる。エディタ上でも赤線で知らせてくれる。</p>
<p>いっぽうTerraformの<code>terraform_remote_state</code>の<code>config</code>は、bucket名やkeyをtypoしていても構文的には通るので、<code>plan</code>を実行してbackendを叩きに行くまで間違いに気付けない。</p>
<h3>呼び出しが短い</h3>
<p><code>let network = upstream_state { source = '../network' }</code>と束縛しておけば、参照は<code>network.vpc_id</code>で済む。<code>data.terraform_remote_state.</code>や<code>.outputs.</code>のような定型的な部分がない。</p>
<p><code>let</code>で束縛する名前は自由につけられるので、短くしたければ<code>net</code>でも何でもいい。</p>
<h3>補完が効く</h3>
<p><code>upstream_state</code>は<code>source</code>でupstreamのディレクトリを指しているので、Carinaはupstreamの<code>.crn</code>ファイルを直接読める。stateを読まずとも、<code>exports</code>ブロックの宣言だけで、何が公開されているかが分かる。</p>
<p>さらにCarinaのDSLは静的な型を持っているので、<code>exports</code>で公開される値にも型がつく。型がついていれば、参照側で必要としている型と照合できる。</p>
<p>たとえば参照側で<code>awscc.ec2.SecurityGroup</code>の<code>vpc_id</code>（型は<code>VpcId</code>）を埋めようとすると、同じ<code>VpcId</code>型を持つupstreamのexportsがLSPの補完候補に上がってくる。</p>
<p><img src="/images/2026/05/upstream-state-completion.png" alt="upstream_stateのexportsがLSPの補完候補に出る様子" /></p>
<p>存在しないexportを参照すればエラーになる。typoしている場合は近い名前の候補も提示される。</p>
<p><img src="/images/2026/05/upstream-state-undefined-export.png" alt="存在しないexportを参照したときのエラー" /></p>
<p>型が合わない場合もエラーになる（<code>vpc_id: String = main.vpc_id</code>のように型アノテーションをつけた場合の例）。</p>
<p><img src="/images/2026/05/upstream-state-type-mismatch.png" alt="型が合わないときのエラー" /></p>
<p>未定義のexport参照や型不一致は<code>carina validate</code>でもエラーになるので、<code>plan</code>や<code>apply</code>を待たずに検出できる。<code>source</code>パスが存在しない場合も同様に<code>carina validate</code>で検出される。</p>
<h3>参照元が未applyでもplanできる</h3>
<p><code>carina validate</code>はstateの内容は見ず、正しい型でexportされているかどうかだけをチェックするので、参照元が未applyでも当然動く。ではplanの場合はどうか。</p>
<p>upstream側のstate自体がまだ存在しない状態でplanを実行すると、こんな出力になる。</p>
<pre><code><span style="color: #d4a017;">Warning: upstream_state 'network': no state found at /path/to/network;
dependent values will display as `(known after upstream apply: ...)`</span>

Refreshing state...
  ✓ awscc.ec2.SecurityGroup.ec2_security_group_39f08cea [0.0s]

Execution Plan:

  + awscc.ec2.SecurityGroup ec2_security_group_39f08cea
      group_description: "Web security group"
      tags:
        Name: "web-sg"
      <span style="color: #00aaaa;">vpc_id: (known after upstream apply: network.vpc_id)</span>
      group_id: (known after apply)
      id: (known after apply)

Plan: 1 to add, 0 to change, 0 to destroy.
</code></pre>
<p>upstream側のstateが無くてもplanは止まらず、参照値は<code>(known after upstream apply: network.vpc_id)</code>というプレースホルダで表示される。「この値はupstreamを<code>apply</code>すれば確定する」というのが<code>plan</code>時点で見える。</p>
<p>upstream側のstateは存在するが、新しいexport値がまだ入っていない（exportを追加してまだ再applyしていない）場合も、Warningが出ないだけで参照値は同じプレースホルダで表示される。</p>
<pre><code>Refreshing state...
  ✓ awscc.ec2.SecurityGroup.ec2_security_group_39f08cea [0.0s]

Execution Plan:

  + awscc.ec2.SecurityGroup ec2_security_group_39f08cea
      group_description: "Web security group"
      tags:
        Name: "web-sg"
      <span style="color: #00aaaa;">vpc_id: (known after upstream apply: network.vpc_id)</span>
      group_id: (known after apply)
      id: (known after apply)

Plan: 1 to add, 0 to change, 0 to destroy.
</code></pre>
<p>Terraformでは参照先のstateが無いとremote stateの読み込みで失敗してplan自体が出せないが、Carinaはプレースホルダで表示するので、planは出せる。前述の<code>try()</code>での回避策のように「実際とは違うplan」が表示されるわけでもない。upstreamを<code>apply</code>した後にもう一度planを取れば、プレースホルダが実際の値に置き換わるだけ。</p>
<hr />
<h2>まとめ</h2>
<p>Terraformのremote stateで気になる点と、Carinaの<code>upstream_state</code>での対応を並べるとこんな感じ。</p>
<table>
<thead>
<tr>
<th>Terraformのremote state</th>
<th>Carinaのupstream_state</th>
</tr>
</thead>
<tbody>
<tr>
<td>参照側がbackend設定を書く必要があり、設定のtypoは<code>plan</code>まで気付けない</td>
<td><code>source</code>にディレクトリを指定するだけ。パスの間違いは<code>carina validate</code>で検出できる</td>
</tr>
<tr>
<td><code>data.terraform_remote_state.network.outputs.vpc_id</code>と長い</td>
<td><code>let</code>で束縛して<code>network.vpc_id</code></td>
</tr>
<tr>
<td><code>outputs</code>の中身はstateを読むまで分からず補完が効かない</td>
<td><code>exports</code>は型付きで補完が効く</td>
</tr>
<tr>
<td>typoや存在しないoutput参照は<code>plan</code>/<code>apply</code>まで気付けない</td>
<td>未定義のexport参照や型不一致は<code>carina validate</code>で検出できる</td>
</tr>
<tr>
<td>参照元が未applyだとplanすら失敗する</td>
<td><code>(known after upstream apply: ...)</code>として未確定値で表示され、planは通る</td>
</tr>
</tbody>
</table>

]]></content>
    </entry>

    <entry>
        <title type="html"><![CDATA[Carina aws providerとawscc providerのベンチマーク]]></title>
        <link href="https://mizzy.org/blog/2026/04/09/1/"/>
        <updated>2026-04-09T14:51:03+09:00</updated>
        <id>https://mizzy.org/blog/2026/04/09/1/</id>
        <content type="html"><![CDATA[
<p><a href="https://github.com/carina-rs/carina">Carina</a>のaws provider（SDK直接呼び出し）とawscc provider（Cloud Control API）のパフォーマンスを比較した。</p>
<p>以前、<a href="https://mizzy.org/blog/2026/01/24/2/">AWS Cloud Control APIは遅い？</a>という記事で、Cloud Control APIが直接SDKを叩くより大幅に遅いことを書いた。あの時点では「Carinaでの採用はやめた」と書いたが、その後、統一的なインターフェースのメリットからawscc providerとして復活させた。今回は、両方のproviderが揃ったので、実際のCarina上でのベンチマークを取った。</p>
<hr />
<h2>計測環境</h2>
<ul>
<li>リージョン: ap-northeast-1</li>
<li>各操作3回実行の平均値</li>
<li>Carinaのオーバーヘッド（パース、diff、state I/O）を含む</li>
</ul>
<hr />
<h2>単一リソースの比較</h2>
<table>
<thead>
<tr>
<th>リソース</th>
<th>操作</th>
<th>aws</th>
<th>awscc</th>
<th>倍率</th>
</tr>
</thead>
<tbody>
<tr>
<td>S3 Bucket</td>
<td>Create</td>
<td>2.0s</td>
<td>16.0s</td>
<td>7.7x</td>
</tr>
<tr>
<td>S3 Bucket</td>
<td>Read</td>
<td>376ms</td>
<td>921ms</td>
<td>2.4x</td>
</tr>
<tr>
<td>S3 Bucket</td>
<td>Destroy</td>
<td>900ms</td>
<td>6.1s</td>
<td>6.8x</td>
</tr>
<tr>
<td>EC2 VPC</td>
<td>Create</td>
<td>1.1s</td>
<td>16.1s</td>
<td>13.8x</td>
</tr>
<tr>
<td>EC2 VPC</td>
<td>Read</td>
<td>382ms</td>
<td>578ms</td>
<td>1.5x</td>
</tr>
<tr>
<td>EC2 VPC</td>
<td>Destroy</td>
<td>973ms</td>
<td>5.7s</td>
<td>5.9x</td>
</tr>
<tr>
<td>EC2 Security Group</td>
<td>Create</td>
<td>1.9s</td>
<td>26.5s</td>
<td>13.4x</td>
</tr>
<tr>
<td>EC2 Security Group</td>
<td>Read</td>
<td>434ms</td>
<td>712ms</td>
<td>1.6x</td>
</tr>
<tr>
<td>EC2 Security Group</td>
<td>Destroy</td>
<td>1.1s</td>
<td>10.8s</td>
<td>9.5x</td>
</tr>
</tbody>
</table>
<p>単一リソースだと、以前の記事と同じ傾向。Createで8-14倍、Destroyで6-10倍、Readで1.5-2.4倍の差がある。Cloud Control APIのresource stabilization（リソースの安定化待ち）が主な原因。</p>
<hr />
<h2>複数リソースの比較</h2>
<p>単一リソースの比較だけだと実運用のパフォーマンスはわからない。実際のインフラは複数のリソースが依存関係を持って構成される。そこで、VPC + サブネット + ルートテーブル + NAT Gateway + セキュリティグループなど、16リソースで構成されるVPCまわり一式でもベンチマークを取った。</p>
<table>
<thead>
<tr>
<th>操作</th>
<th>aws</th>
<th>awscc</th>
<th>倍率</th>
</tr>
</thead>
<tbody>
<tr>
<td>Apply（16リソース）</td>
<td>130.2s</td>
<td>152.5s</td>
<td>1.2x</td>
</tr>
<tr>
<td>Plan（全リソースread）</td>
<td>0.7s</td>
<td>1.4s</td>
<td>2.0x</td>
</tr>
<tr>
<td>Destroy（16リソース）</td>
<td>57.9s</td>
<td>69.3s</td>
<td>1.2x</td>
</tr>
</tbody>
</table>
<p>単一リソースでは8-14倍の差があったのが、16リソースになると1.2倍まで縮まった。</p>
<hr />
<h2>なぜ差が縮まるのか</h2>
<p>Carinaは依存関係のないリソースを並列に実行する。16リソースのうち、互いに依存しないものは同時にAPIを叩く。すると、ボトルネックは個々のAPI呼び出しの速度ではなく、依存チェーンの中で最も遅いリソースになる。この構成ではNAT Gatewayの作成（約90秒）がボトルネックで、これはawsでもawsccでも同じ。</p>
<p>つまり、並列実行が個々のAPIの遅さを吸収してしまう。</p>
<hr />
<h2>まとめ</h2>
<ul>
<li>単一リソースではaws providerが圧倒的に速い（8-14倍）</li>
<li>複数リソースの実運用シナリオでは差が1.2倍まで縮まる</li>
<li><code>carina plan</code>はReadが主なので、aws providerが1.5-2倍速い</li>
</ul>
<p>単一リソースの数字だけ見るとawscc providerは使い物にならないように見えるが、実運用では並列実行のおかげで差はかなり小さい。awscc providerはCloudFormationスキーマからの自動生成でカバレッジが広いというメリットがあるので、パフォーマンスだけで選ぶ必要はない。</p>

]]></content>
    </entry>

    <entry>
        <title type="html"><![CDATA[CarinaのProvider Plugin]]></title>
        <link href="https://mizzy.org/blog/2026/04/06/1/"/>
        <updated>2026-04-06T18:53:12+09:00</updated>
        <id>https://mizzy.org/blog/2026/04/06/1/</id>
        <content type="html"><![CDATA[
<p><a href="https://github.com/carina-rs/carina">Carina</a>のProvider Pluginの仕組みについて書いておく。最初はTerraformと同じプロセスベースの方式で実装し、その後Wasmに移行した。</p>
<hr />
<h2>Terraformのprovider plugin</h2>
<p>Terraformのproviderは、<a href="https://github.com/hashicorp/go-plugin">go-plugin</a>というフレームワークを使って、外部プロセスとして動作する。Terraform本体がproviderのバイナリを子プロセスとして起動し、gRPCで通信する仕組みになっている。</p>
<hr />
<h2>Carinaでも同じ方式で実装した</h2>
<p>Carinaでは当初、providerのコードがcarina-cli本体に直接組み込まれていた。これをTerraformと同じアプローチで外部プロセスに分離した。providerを子プロセスとして起動し、stdin/stdoutでJSON-RPCを使って通信する方式。</p>
<pre><code>Carina (Host)                    Provider (Child Process)
    |                                    |
    |-- spawn process ------------------&gt;|
    |                                    |-- ready
    |-- JSON-RPC: {&quot;method&quot;:&quot;create&quot;} --&gt;|
    |                                    |-- calls AWS SDK directly
    |&lt;-- JSON-RPC: {&quot;result&quot;: State} ----|
    |                                    |
    |-- JSON-RPC: {&quot;method&quot;:&quot;shutdown&quot;}-&gt;|
    |                                    |-- exit
</code></pre>
<p>実装は3つのクレートに分けた。</p>
<ul>
<li><strong>carina-plugin-host</strong> — プロセスの起動とJSON-RPCクライアント</li>
<li><strong>carina-plugin-sdk</strong> — provider開発者向けのSDK。<code>CarinaProvider</code>トレイトを実装して<code>carina_plugin_sdk::run()</code>を呼ぶだけでproviderになる</li>
<li><strong>carina-provider-protocol</strong> — JSON-RPCメッセージの型定義</li>
</ul>
<p>この方式で、aws providerとawscc providerをそれぞれ外部プロセスとして動作させるところまで実装した。</p>
<hr />
<h2>Wasmへの移行</h2>
<p>プロセスベースで動くようにはなったが、Wasmに興味があったので、providerをWasmに移行することにした。</p>
<pre><code>carina-cli
  └── WasmProviderFactory (carina-plugin-host)
        └── Wasmtime runtime
              └── Wasm Component (.wasm)
                    ├── CarinaProvider WIT interface
                    └── wasi:http/outgoing-handler (AWS API呼び出し用)
</code></pre>
<p>実用的なメリットもある。OS/arch別の6+バイナリが単一の<code>.wasm</code>ファイルになるので、配布が楽になる。WASIのサンドボックスにより、providerのファイルシステムやネットワークアクセスを制限できるのもいい。</p>
<h3>WIT (Wasm Interface Types) によるインターフェース定義</h3>
<p>プロセスベースではJSON-RPCでメッセージを交換していたが、WasmではWITでインターフェースを定義する。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="k">interface</span> <span class="nv">provider</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">use</span> <span class="nv">types</span><span class="p">.{</span> <span class="k">resource</span><span class="err">-</span><span class="nv">id</span><span class="p">,</span> <span class="nv">state</span><span class="p">,</span> <span class="k">resource</span><span class="err">-</span><span class="nv">def</span><span class="p">,</span> <span class="nv">value</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="na">info</span><span class="p">:</span> <span class="k">func</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">string</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="na">schemas</span><span class="p">:</span> <span class="k">func</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">string</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="na">validate-config</span><span class="p">:</span> <span class="k">func</span><span class="p">(</span><span class="na">attrs</span><span class="p">:</span> <span class="kt">list</span><span class="p">&lt;</span><span class="kt">tuple</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="nv">value</span><span class="p">&gt;&gt;)</span> <span class="o">-&gt;</span> <span class="kt">result</span><span class="p">&lt;</span><span class="nb">_</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;;</span>
</span></span><span class="line"><span class="cl">    <span class="na">initialize</span><span class="p">:</span> <span class="k">func</span><span class="p">(</span><span class="na">attrs</span><span class="p">:</span> <span class="kt">list</span><span class="p">&lt;</span><span class="kt">tuple</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="nv">value</span><span class="p">&gt;&gt;)</span> <span class="o">-&gt;</span> <span class="kt">result</span><span class="p">&lt;</span><span class="nb">_</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="na">read</span><span class="p">:</span> <span class="k">func</span><span class="p">(</span><span class="na">id</span><span class="p">:</span> <span class="k">resource</span><span class="err">-</span><span class="nv">id</span><span class="p">,</span> <span class="na">identifier</span><span class="p">:</span> <span class="kt">option</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;)</span> <span class="o">-&gt;</span> <span class="kt">result</span><span class="p">&lt;</span><span class="nv">state</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;;</span>
</span></span><span class="line"><span class="cl">    <span class="na">create</span><span class="p">:</span> <span class="k">func</span><span class="p">(</span><span class="na">res</span><span class="p">:</span> <span class="k">resource</span><span class="err">-</span><span class="nv">def</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">result</span><span class="p">&lt;</span><span class="nv">state</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;;</span>
</span></span><span class="line"><span class="cl">    <span class="na">update</span><span class="p">:</span> <span class="k">func</span><span class="p">(</span><span class="na">id</span><span class="p">:</span> <span class="k">resource</span><span class="err">-</span><span class="nv">id</span><span class="p">,</span> <span class="na">identifier</span><span class="p">:</span> <span class="kt">string</span><span class="p">,</span> <span class="na">current</span><span class="p">:</span> <span class="nv">state</span><span class="p">,</span> <span class="na">to</span><span class="p">:</span> <span class="k">resource</span><span class="err">-</span><span class="nv">def</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">result</span><span class="p">&lt;</span><span class="nv">state</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;;</span>
</span></span><span class="line"><span class="cl">    <span class="na">delete</span><span class="p">:</span> <span class="k">func</span><span class="p">(</span><span class="na">id</span><span class="p">:</span> <span class="k">resource</span><span class="err">-</span><span class="nv">id</span><span class="p">,</span> <span class="na">identifier</span><span class="p">:</span> <span class="kt">string</span><span class="p">,</span> <span class="na">options</span><span class="p">:</span> <span class="kt">string</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">result</span><span class="p">&lt;</span><span class="nb">_</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>プロセスベースのJSON-RPCと比べると、通信プロトコル自体がWITで定義されるようになった。ただし、WITは再帰的な型をサポートしていないため、ListやMapの値はJSON文字列としてやり取りしている。</p>
<h3>WasmからのHTTPアクセス</h3>
<p>Wasmはサンドボックス環境なので、OSのシステムコール（TCPソケットなど）に直接アクセスできない。ネイティブのHTTPクライアントはOSのソケットAPIに依存しているため、そのままではWasm内で動かない。</p>
<p>代わりに、WASIが提供する<code>wasi:http/outgoing-handler</code>というインターフェースを使う。provider内のHTTPリクエストはこのインターフェースを通じてホスト側に委譲され、ホスト側が実際のHTTP通信を行う。</p>
<pre class="mermaid">graph LR
    subgraph &#34;Wasm Sandbox&#34;
        A[&#34;AWS SDK\ns3_client.get_object()&#34;] --&gt; B[&#34;WasiHttpClient&#34;]
    end
    subgraph &#34;Host (Carina)&#34;
        B --&gt;|&#34;wasi:http/outgoing-handler&#34;| C[&#34;Wasmtime HTTP実装&#34;]
    end
    C --&gt; D[&#34;AWS API\n.amazonaws.com&#34;]
</pre><p>AWS SDKはHTTPクライアントを差し替え可能な設計になっているので、<code>#[cfg(target_arch = &quot;wasm32&quot;)]</code>で条件分岐し、Wasmビルド時だけ<code>wasi:http/outgoing-handler</code>を使う<code>WasiHttpClient</code>を差し込んでいる。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="cp">#[cfg(not(target_arch = </span><span class="s">&#34;wasm32&#34;</span><span class="cp">))]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">async</span><span class="w"> </span><span class="k">fn</span> <span class="nf">build_config</span><span class="p">(</span><span class="n">region</span>: <span class="kp">&amp;</span><span class="kt">str</span><span class="p">)</span><span class="w"> </span>-&gt; <span class="nc">aws_config</span>::<span class="n">SdkConfig</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">aws_config</span>::<span class="n">defaults</span><span class="p">(</span><span class="n">aws_config</span>::<span class="n">BehaviorVersion</span>::<span class="n">latest</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">region</span><span class="p">(</span><span class="n">Region</span>::<span class="n">new</span><span class="p">(</span><span class="n">region</span><span class="p">.</span><span class="n">to_string</span><span class="p">()))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">load</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="k">await</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="cp">#[cfg(target_arch = </span><span class="s">&#34;wasm32&#34;</span><span class="cp">)]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">async</span><span class="w"> </span><span class="k">fn</span> <span class="nf">build_config</span><span class="p">(</span><span class="n">region</span>: <span class="kp">&amp;</span><span class="kt">str</span><span class="p">)</span><span class="w"> </span>-&gt; <span class="nc">aws_config</span>::<span class="n">SdkConfig</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">use</span><span class="w"> </span><span class="n">carina_plugin_sdk</span>::<span class="n">wasi_http</span>::<span class="n">WasiHttpClient</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">aws_config</span>::<span class="n">defaults</span><span class="p">(</span><span class="n">aws_config</span>::<span class="n">BehaviorVersion</span>::<span class="n">latest</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">region</span><span class="p">(</span><span class="n">Region</span>::<span class="n">new</span><span class="p">(</span><span class="n">region</span><span class="p">.</span><span class="n">to_string</span><span class="p">()))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">http_client</span><span class="p">(</span><span class="n">WasiHttpClient</span>::<span class="n">new</span><span class="p">())</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="n">load</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">.</span><span class="k">await</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre><p>この差し替えだけで、AWS SDKの呼び出しコード（<code>s3_client.get_object()</code>など）自体は変更不要。</p>
<p>通信がすべてホスト側を経由するということは、ホスト側でアクセス先を制御できるということでもある。</p>
<h3>Carina側で追加している制限</h3>
<p>プロセスベースの方式では、providerはネイティブバイナリとして動作するので、ファイルシステムやネットワークに自由にアクセスできる。ホスト側でそれを制限する手段がない。Wasmに移行したことで、providerに対して以下のような制限をかけられるようになった。</p>
<ul>
<li><strong>HTTP許可リスト</strong> — <code>.amazonaws.com</code>ドメインとEC2/ECSメタデータエンドポイントのみアクセス可能</li>
<li><strong>メモリ制限</strong> — 256MB上限</li>
<li><strong>タイムアウト</strong> — 各操作に30秒のepochベースタイムアウト</li>
<li><strong>ソケット排除</strong> — <code>wasi:sockets</code>は提供しない</li>
<li><strong>環境変数</strong> — AWS認証情報関連のみ許可リストで公開</li>
</ul>
<p>ファイルシステムアクセスも一切許可していない。AWSの認証情報はホスト側で取得し、環境変数としてWasm環境に明示的に渡す。</p>
<h3>プリコンパイルキャッシュ</h3>
<p>Wasmの初回ロードは<code>.wasm</code>ファイルのコンパイルが必要だが、2回目以降はプリコンパイル済みの<code>.cwasm</code>ファイルをキャッシュして使う。</p>
<pre><code>~/.carina/providers/
  └── github.com/carina-rs/carina-provider-aws/
      └── v0.5.0/
          ├── carina-provider-aws.wasm      # オリジナル
          └── carina-provider-aws.cwasm     # プリコンパイル済み
</code></pre>
<p><code>.cwasm</code>はWasmtimeのバージョンに依存するので、Wasmtimeのバージョンが変わったら再生成する。</p>
<hr />
<h2>現状</h2>
<p>現時点では、aws providerとawscc providerの両方がWasmで動作している。プロセスベースの実装はすでに削除済み。</p>
<p>Wasmへの移行は単純に技術的な興味からだったが、結果的にサンドボックスによるセキュリティが手に入ったのはよい副産物だった。サードパーティproviderを受け入れる上で、providerのコードが任意のファイルやネットワークにアクセスできないことが保証されているのは安心感がある。</p>

]]></content>
    </entry>

    <entry>
        <title type="html"><![CDATA[法人化して10年が経った]]></title>
        <link href="https://mizzy.org/blog/2026/04/01/1/"/>
        <updated>2026-04-01T13:26:35+09:00</updated>
        <id>https://mizzy.org/blog/2026/04/01/1/</id>
        <content type="html"><![CDATA[
<p><img src="/images/2026/04/bookkeeping-shelf.jpg" alt="領収書と10年分の決算報告書" /></p>
<p>領収書と10年分の決算報告書。領収書はこれ以外にもたくさんある。法人化してからの年月が物理的に可視化されている棚。</p>
<hr />
<p>4月1日に備忘録的にふりかえりエントリを書いていたが、<a href="/blog/2021/04/01/1/">前回</a> から5年も空いてしまった。</p>
<p>フリーランスになったのが2014年4月なので、フリーランスとしては12年。<a href="/blog/2016/04/01/1/">フリーランスになって2年が経った</a> で書いたように2016年2月に法人登記したので、法人化してからは10年が経ったことになる。フリーランス12年より法人化10年の方がキリがいいので、今回はそちらをタイトルにした。</p>
<hr />
<h2>12年続いている理由</h2>
<p><a href="https://note.com/suthio/n/n7b889ef3bbf9">僕がフリーランスを続けなかった構造的な理由</a> というすてぃおさんの記事との対比で書いてみる。すてぃおさんが1年半のフリーランス経験を経て、業務委託として働くスタイルを長期的にはオススメできない、と書いていて、構造的な問題として挙げられているのは以下の3つ。</p>
<ol>
<li>挑戦させてもらいづらい</li>
<li>社会的な評価が蓄積されにくい</li>
<li>マネジメントや組織の意思決定に関われない</li>
</ol>
<p>加えて、「即戦力の罠」によるスキルの切り売り感や、社会資本が蓄積されにくい、といった問題も挙げられている。例外として「希少人材」であれば構造を超えられる可能性がある、とも。</p>
<p>自分は12年続いている側なので、それぞれについて書いてみる。</p>
<p>まず「挑戦させてもらいづらい」について。自分の場合、経験のないプログラミング言語や技術領域の仕事を任せてもらうこともある。なので、この問題はあてはまらない。「確実にできることしか任せてもらえない」という状況にはなっていない。また、同じ技術領域であっても会社によって解決したい課題は異なるので、そういう意味でも新しい経験は得られている。同様に「即戦力の罠」によるスキルの切り売り感もあまりない。</p>
<p>「社会的な評価が蓄積されにくい」については、もう十分蓄積されているので、これ以上蓄積しなくてもよさそう、というのが正直なところ。</p>
<p>「マネジメントや組織の意思決定に関われない」については、むしろマネジメントしたくないからフリーランスをやっている。これはデメリットではなく、自分にとってはメリット。</p>
<p>「社会資本が蓄積されない」については、自分の場合、そもそも信頼関係や評判の基盤が会社ではなくオープンな技術コミュニティにあるので、契約が終わったらリセットされる、ということがない。</p>
<p>元記事では「希少人材であれば構造を超えられる」とあるが、まあ、自分はそこに該当するのだと思う。自分で言うのもどうかと思うけど、12年続いているという事実がそれを裏付けているのだろう。</p>
<p>ただ、元記事の主張自体は理解できる。自分のケースが一般化できるとは思っていない。</p>
<hr />
<h2>関連エントリ</h2>
<ul>
<li><a href="https://mizzy.org/blog/2021/04/01/1/">大学院に入学した/フリーランスになって7年が経った - Gosuke Miyashita</a></li>
<li><a href="https://mizzy.org/blog/2019/04/01/1/">フリーランスになって5年が経った - Gosuke Miyashita</a></li>
<li><a href="https://mizzy.org/blog/2018/04/02/1/">フリーランスになって4年が経った - Gosuke Miyashita</a></li>
<li><a href="https://mizzy.org/blog/2017/04/01/1/">フリーランスになって3年が経った - Gosuke Miyashita</a></li>
<li><a href="https://mizzy.org/blog/2016/04/01/1/">フリーランスになって2年が経った - Gosuke Miyashita</a></li>
<li><a href="https://mizzy.org/blog/2015/04/01/1/">フリーランスになって1年が経った - Gosuke Miyashita</a></li>
</ul>

]]></content>
    </entry>

    <entry>
        <title type="html"><![CDATA[Carinaのシークレット管理]]></title>
        <link href="https://mizzy.org/blog/2026/03/28/1/"/>
        <updated>2026-03-28T14:52:19+09:00</updated>
        <id>https://mizzy.org/blog/2026/03/28/1/</id>
        <content type="html"><![CDATA[
<p><a href="https://github.com/carina-rs/carina">Carina</a>にシークレット管理の仕組みを設計・実装しているので、Terraformとの設計の違いについて書いておく。</p>
<hr />
<h2>Terraformのシークレット問題</h2>
<p>Terraformにおけるシークレット管理は長年の課題だった。<a href="https://github.com/hashicorp/terraform/issues/516">GitHub issue #516</a>は2014年10月に作成され、250以上のコメントがついている。</p>
<p>問題の根本は、Terraformのstateにシークレットが平文で保存されることにある。<code>sensitive</code>フラグをつけても、plan出力がマスクされるだけで、stateには平文のまま残る。<code>aws_kms_secrets</code>データソースを使っても、復号結果はstateに平文で保存される。</p>
<p>この問題に対してTerraformは段階的に対処してきた。</p>
<ul>
<li><strong>sensitive</strong>（Terraform 0.14、2020年）: plan出力のマスク。stateは平文のまま</li>
<li><strong>ephemeral</strong>（Terraform 1.10、2024年11月）: stateに一切保存しない値。<code>ephemeral</code>リソースやephemeral変数として使う</li>
<li><strong>write-only</strong>（Terraform 1.11、2025年3月）: リソース属性の値をstateに保存しない仕組み</li>
</ul>
<p>問題提起から約10年かけて、ようやく「stateに保存しない」という本質的な解決策にたどり着いた形になる。</p>
<hr />
<h2>Terraformのwrite-only</h2>
<p>write-onlyはTerraform 1.11で導入された仕組みで、リソースの属性値をstateに一切保存しないようにする。例えばSSM Parameterにシークレットを保存する場合、従来は値がstateに平文で残っていたが、write-onlyを使うとこうなる。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;aws_kms_secrets&#34; &#34;example&#34;</span> {
</span></span><span class="line"><span class="cl">  <span class="k">secret</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span>    <span class="o">=</span> <span class="s2">&#34;db_password&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    payload</span> <span class="o">=</span> <span class="s2">&#34;AQICAHh...&#34;</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_ssm_parameter&#34; &#34;db_password&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span>             <span class="o">=</span> <span class="s2">&#34;/myapp/db_password&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  type</span>             <span class="o">=</span> <span class="s2">&#34;SecureString&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  value_wo</span>         <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_kms_secrets</span><span class="p">.</span><span class="k">example</span><span class="p">.</span><span class="k">plaintext</span><span class="p">[</span><span class="s2">&#34;db_password&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">  value_wo_version</span> <span class="o">=</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">}
</span></span></code></pre><p>stateに値を保存しないので安全だが、代わりに変更検知ができなくなるという問題がある。そこで<code>value_wo_version</code>という数値を手動で管理する。値を変更したらこれを1から2にインクリメントする。Terraformはこのバージョン番号の変更を検知して、プロバイダーに新しい値を送る。</p>
<p>実際にversionを変更してplanすると、こうなる。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="c1">  # aws_ssm_parameter.db_password will be updated in-place
</span></span></span><span class="line"><span class="cl">  <span class="err">~</span> <span class="k">resource</span> <span class="s2">&#34;aws_ssm_parameter&#34; &#34;db_password&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">      ~ has_value_wo</span>     <span class="o">=</span> <span class="kt">true</span> <span class="err">-&gt;</span> <span class="p">(</span><span class="k">known</span> <span class="k">after</span> <span class="k">apply</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">        id</span>               <span class="o">=</span> <span class="s2">&#34;/tf-write-only-test/db_password&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">        name</span>             <span class="o">=</span> <span class="s2">&#34;/tf-write-only-test/db_password&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">      ~ value_wo_version</span> <span class="o">=</span> <span class="m">1</span> <span class="err">-&gt;</span> <span class="m">2</span><span class="c1">
</span></span></span><span class="line"><span class="cl"><span class="c1">        # (12 unchanged attributes hidden)
</span></span></span><span class="line"><span class="cl">    }
</span></span></code></pre><p><code>value_wo_version</code>の変更は表示されるが、<code>value_wo</code>自体の値や変更内容は一切表示されない。</p>
<p>この設計にはいくつかの問題がある。</p>
<ul>
<li>バージョンのインクリメントを忘れると、実際の値が変わっていてもTerraformは変更を検知しない</li>
<li>リソースタイプごとに個別にwrite-only対応が必要で、対応はまだ一部のリソースに限られる</li>
<li><code>_wo</code>サフィックスの命名規則により、既存の属性名とは別の属性が必要になる</li>
</ul>
<p>なお、<a href="https://developer.hashicorp.com/terraform/plugin/framework/resources/write-only-arguments">プラグインフレームワークのドキュメント</a>では、private stateにハッシュを保存して変更を検知するアプローチもベストプラクティスとして紹介されている。ただし、少なくともAWS providerでは現時点ではこの方式は採用されていないようだ。</p>
<hr />
<h2>Carinaのsecret() + decrypt()</h2>
<p>Carinaでは同じことを<code>secret()</code>（<a href="https://github.com/carina-rs/carina/issues/1238">#1238</a>）と<code>decrypt()</code>（<a href="https://github.com/carina-rs/carina/issues/1240">#1240</a>）の組み合わせで実現する。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="nc">awscc.ssm.parameter</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">name</span>  <span class="o">=</span> <span class="s2">&#34;/myapp/db_password&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">type</span>  <span class="o">=</span> <span class="s2">&#34;SecureString&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="na">value</span> <span class="o">=</span> <span class="nv">secret</span><span class="p">(</span><span class="nv">decrypt</span><span class="p">(</span><span class="s2">&#34;AQICAHh...&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><p><code>decrypt()</code>はapply時にプロバイダーの暗号化サービス（AWS KMS等）を使って復号する。<code>secret()</code>で包むことで、復号後の値はstateにハッシュとしてのみ保存される。</p>
<p><code>secret()</code>の動作は以下の通り。</p>
<ul>
<li><strong>apply時</strong>: <code>secret()</code>をアンラップして実際の値をプロバイダーに送る</li>
<li><strong>state</strong>: 値そのものではなくArgon2idハッシュのみを保存する。ソルトはリソースタイプ・リソース名・属性キーから生成するので、同じ値でもリソースごとに異なるハッシュになる</li>
<li><strong>plan出力</strong>: 値の代わりに<code>(secret)</code>と表示する。変更時は<code>(secret) → (secret)</code>と表示され、値は見えないが変更があったことはわかる</li>
<li><strong>diff</strong>: ハッシュの比較で変更を自動検知する</li>
</ul>
<p>Terraformのwrite-onlyとの違いをまとめると以下の通り。</p>
<ul>
<li>ハッシュをstateに保存することで変更検知を自動化。バージョン番号を手動で管理する必要がない</li>
<li>ハッシュから元の値を復元することはできないので、stateからシークレットが漏洩するリスクもない</li>
<li><code>secret()</code>は組み込み関数なので、プロバイダー側の個別対応は不要。どのリソースのどの属性にも使える</li>
<li>Terraformでは復号用のデータソースを別途定義する必要があるが、Carinaでは属性値に直接書ける</li>
<li><code>decrypt()</code>はプロバイダー非依存の設計で、AWS KMS、GCP Cloud KMS、Azure Key Vaultなどプロバイダーの暗号化サービスに自動的に委譲する予定</li>
</ul>
<hr />
<h2>env()との組み合わせ</h2>
<p>暗号文を<code>.crn</code>ファイルに書きたくない場合は、<code>env()</code>関数（<a href="https://github.com/carina-rs/carina/issues/1239">#1239</a>、実装済み）で環境変数から渡すこともできる。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="nc">awscc.rds.db_instance</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">master_password</span> <span class="o">=</span> <span class="nv">secret</span><span class="p">(</span><span class="nv">env</span><span class="p">(</span><span class="s2">&#34;DB_PASSWORD&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><hr />
<h2>設計の違いまとめ</h2>
<p>Carinaのシークレット管理は、<code>secret()</code>と<code>decrypt()</code>の2つの関数で実現する。</p>
<ul>
<li><code>secret(value)</code> — 値をシークレットとしてマーク。stateにはハッシュのみ保存</li>
<li><code>decrypt(ciphertext)</code> — プロバイダーの暗号化サービスで復号</li>
</ul>
<p>これらに<code>env()</code>のような汎用関数を組み合わせることもできる。それぞれが単一の責務を持ち、自由に組み合わせられる。Terraformでは<code>sensitive</code>、<code>ephemeral</code>、write-only、<code>aws_kms_secrets</code>データソースと、歴史的経緯から複数の仕組みが並立している。Carinaは後発の利点を活かして、最初からシンプルな設計にできた、と思う。</p>
<p>ただし、これはあくまで現段階の設計であり、より安全で良い方法が見つかれば変更する可能性はある。</p>

]]></content>
    </entry>

    <entry>
        <title type="html"><![CDATA[Carinaのplan表示を改善した]]></title>
        <link href="https://mizzy.org/blog/2026/03/22/1/"/>
        <updated>2026-03-22T19:21:17+09:00</updated>
        <id>https://mizzy.org/blog/2026/03/22/1/</id>
        <content type="html"><![CDATA[
<p>Carinaの<code>carina plan</code>コマンドの表示を最近まとめて改善したので、その内容を書いておく。</p>
<hr />
<h2>スナップショットテストの導入</h2>
<p>plan表示の改善を始める前に、まずテストの仕組みを整えた。plan表示は目視で確認するのが面倒だし、変更のたびに既存の表示が壊れていないか確認するのも大変なので、<a href="https://insta.rs/">insta</a>を使ったスナップショットテストを導入した。</p>
<p>仕組みはシンプルで、テスト用の<code>.crn</code>ファイル（とオプションでstateファイル）をfixtureとして用意し、それに対して<code>format_plan()</code>を実行した結果をスナップショットとして保存する。テスト実行時にスナップショットと一致しなければ失敗する。表示を変更した場合は<code>cargo insta review</code>で差分を確認してスナップショットを更新すれば良い。</p>
<p>plan表示はANSIカラーコードを含むので、スナップショットの可読性のためにカラーコードをstripしてからスナップショットに保存している。</p>
<p>これにより、表示の改善を安心してガンガン進められるようになった。実際のスナップショットは<a href="https://github.com/carina-rs/carina/tree/main/carina-cli/src/snapshots">こちら</a>にあるので、どんな表示になるのかはここを見るとわかる。</p>
<hr />
<h2>依存関係のツリー表示</h2>
<p>Terraformの<code>terraform plan</code>はリソースをフラットなリストで表示する。リソース間に依存関係があっても、出力上はすべて同じ階層にフラットに並ぶ。さらにリソース名のアルファベット順でソートされるので、依存関係のあるリソース同士が離れた位置に表示されることもある。結果として、どのリソースがどのリソースに依存しているかは人間が属性値を見て判断する必要がある。</p>
<p>Carinaではリソース間の依存関係をツリー構造で表示するようにした。</p>
<p><img src="/images/2026/03/carina-plan-tree.png" alt="carina plan のツリー表示" /></p>
<p>VPCの下にroute_tableとsubnetがぶら下がっているのが視覚的にわかる。Create、Update、Destroyが混在する場合でも、依存関係の中で何が起きるかがひと目でわかる。</p>
<p><img src="/images/2026/03/carina-plan-mixed.png" alt="Create、Update、Destroyが混在するplan表示" /></p>
<hr />
<h2>read-only属性の表示</h2>
<p>リソースを作成する際、ARNやIDのようなread-only属性はAWS側が自動的に割り当てる。以前のplan表示ではこれらは完全に省略されていたが、Terraformと同様に<code>(known after apply)</code>として表示するようにした。</p>
<p><img src="/images/2026/03/carina-plan-read-only.png" alt="read-only属性の表示" /></p>
<p>これにより、リソース作成後にどんな属性が生えてくるのかがplanの時点でわかるようになった。</p>
<hr />
<h2>デフォルト値の表示</h2>
<p>スキーマにデフォルト値が定義されている属性も、ユーザが明示的に指定していなければplanに表示されていなかった。これを<code># default</code>アノテーション付きで表示するようにした。</p>
<p><img src="/images/2026/03/carina-plan-default-values.png" alt="デフォルト値の表示" /></p>
<p>ユーザが指定した属性、デフォルト値が適用される属性、apply後に確定するread-only属性、という3層が一目でわかる。</p>
<hr />
<h2>未変更属性のサマリ表示</h2>
<p>Update/Replaceの際、変更されない属性がいくつあるかを<code># (3 unchanged attributes hidden)</code>のようなサマリ行で表示するようにした。</p>
<p><img src="/images/2026/03/carina-plan-map-diff.png" alt="未変更属性のサマリ表示" /></p>
<hr />
<h2><code>--detail</code>フラグによる表示レベルの制御</h2>
<p><code>--detail</code>フラグで表示レベルを制御できるようにした。3つのモードがある。</p>
<ul>
<li><strong>full</strong>（デフォルト）: すべての属性を表示。未変更・デフォルト・read-onlyも含む</li>
<li><strong>explicit</strong>: ユーザが<code>.crn</code>ファイルで明示的に指定した属性のみ表示</li>
<li><strong>none</strong>: リソース名と依存ツリーのみ表示</li>
</ul>
<p>デフォルトの<code>--detail full</code>だとこう。</p>
<p><img src="/images/2026/03/carina-plan-detail-full.png" alt="--detail full の表示" /></p>
<p><code>--detail explicit</code>だとこうなる。</p>
<p><img src="/images/2026/03/carina-plan-detail-explicit.png" alt="--detail explicit の表示" /></p>
<p><code>--detail none</code>だとリソース名と依存ツリーのみになる。リソース数が多いときに全体の構造だけさくっと確認したい場合に便利。</p>
<p><img src="/images/2026/03/carina-plan-detail-none.png" alt="--detail none の表示" /></p>
<hr />
<h2>TUI（Terminal UI）の追加</h2>
<p>CLIの表示とは別に、<code>--tui</code>フラグでTUI（Terminal UI）も使えるようにした。ratatuiとcrosstermで実装している。</p>
<p><video src="/images/2026/03/carina-tui.mp4" controls muted playsinline style="max-width:100%"></video></p>
<p>主な機能は以下の通り。</p>
<ul>
<li><strong>ツリービュー + 詳細パネル</strong>: 画面の70%がリソースの依存関係ツリー、30%が選択したリソースの属性詳細。Tabキーでパネル間を切り替える</li>
<li><strong>検索・フィルタ</strong>: <code>/</code>キーで検索モードに入り、リソース名やタイプでフィルタ。Tabキーで補完、<code>n</code>/<code>N</code>でマッチ間を移動</li>
<li><strong>リソース参照ナビゲーション</strong>: 詳細パネルで<code>vpc_id: vpc.vpc_id</code>のようなリソース参照をEnterキーで辿れる。Backspaceで戻る</li>
<li><strong>色分け</strong>: Create（緑）、Update（黄）、Replace（マゼンタ）、Delete（赤）で操作の種類が一目でわかる</li>
</ul>
<hr />
<p>これらの改善は、Terraformのplan表示を参考にしつつ、ツリー表示やTUIのようなCarina独自の機能も加えている。applyする前にリソースの全体像を把握しやすくなった。</p>

]]></content>
    </entry>

    <entry>
        <title type="html"><![CDATA[Carinaの開発状況（2月下旬〜3月中旬）]]></title>
        <link href="https://mizzy.org/blog/2026/03/20/1/"/>
        <updated>2026-03-20T17:56:56+09:00</updated>
        <id>https://mizzy.org/blog/2026/03/20/1/</id>
        <content type="html"><![CDATA[
<p><a href="https://mizzy.org/blog/2026/02/21/1/">前回の記事</a>ではClaude Codeによるイシュー駆動開発について書いた。あの時点では90個のイシューが登録され67個がクローズされた、というところだったが、あれから約1ヶ月、さらに約280個のイシューがクローズされ、約320個のPRがマージされた。</p>
<p>この1ヶ月でやったことを振り返っておく。</p>
<hr />
<h2>AWS providerの本格整備</h2>
<p>aws providerは以前からあったが、スキーマの手書き部分が多く、テストも不十分だった。この1ヶ月で本格的に整備した。</p>
<p>まず、<a href="https://github.com/aws/api-models-aws">Smithyモデル</a>からスキーマを自動生成する仕組みを入れた（<a href="https://github.com/carina-rs/carina/issues/461">#461</a>）。awscc providerのCloudFormationスキーマからの自動生成と同じ考え方で、AWS SDKの元になっているSmithyモデルから、リソーススキーマとread関数を自動生成する。手書きのスキーマは信用できないので、できるだけ自動生成に寄せていきたい。</p>
<hr />
<h2>Acceptance testの拡充</h2>
<p>AWS providerとAWSCC providerの両方で、acceptance testを大幅に増やした。前回の記事時点ではAWSCC providerの39ファイルだけだったが、AWS providerのテスト追加とシナリオの拡充で172ファイルになった（AWSCC: 120、AWS: 52）。基本的なCRUDに加えて、タグの削除、in-place update、create-before-destroy、属性の削除といったシナリオを整備している。</p>
<p>特に大きかったのは、in-place updateのテストの追加。リソースを作成した後、属性を変更してもう一度applyし、replaceではなくin-place updateが正しく行われることを確認するテスト。これを入れたことで、AWSCC providerのupdate処理にあったバグが大量に見つかった。</p>
<ul>
<li>Cloud Control APIのupdate用patch documentにread-onlyプロパティが含まれていた（<a href="https://github.com/carina-rs/carina/issues/807">#807</a>）</li>
<li>create-onlyプロパティもpatchに含まれていた（<a href="https://github.com/carina-rs/carina/issues/810">#810</a>）</li>
<li>JSON Patchのreplaceではなくaddオペレーションを使うべきだった（<a href="https://github.com/carina-rs/carina/issues/795">#795</a>）</li>
<li>変更のない属性もpatchに含まれていた（<a href="https://github.com/carina-rs/carina/issues/737">#737</a>）</li>
<li>属性の削除がpatchに反映されていなかった（<a href="https://github.com/carina-rs/carina/issues/736">#736</a>）</li>
</ul>
<p>multi-stepテスト（create → update → destroy）のインフラも整備した。テストスクリプトのシグナルハンドリング（<a href="https://github.com/carina-rs/carina/issues/793">#793</a>）、destroy失敗時の検出（<a href="https://github.com/carina-rs/carina/issues/855">#855</a>）、orphanedリソースの防止（<a href="https://github.com/carina-rs/carina/issues/812">#812</a>）など、テストの信頼性を上げるための改善も多い。</p>
<hr />
<h2>Anonymous resourceの識別と置き換え</h2>
<p>Carinaでは、Terraformと異なり、リソースに名前をつけなくてもよい。</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="c"># Terraformだと aws_vpc.main のように名前が必要だが、Carinaでは不要</span>
</span></span><span class="line"><span class="cl"><span class="nc">awscc.ec2_vpc</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="na">cidr_block</span> <span class="o">=</span> <span class="s2">&#34;10.0.0.0/16&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre><p>ただし、内部的には<code>.crn</code>ファイルに書かれたリソースとstateに保存されたリソースを突き合わせるためのidentifierが必要になる。Terraformは人間がつけた名前（<code>aws_vpc.main</code>の<code>main</code>の部分）をidentifierとして使うが、Carinaのanonymous resourceにはそれがない。</p>
<p>そこで、スキーマのcreate-onlyプロパティ（作成後に変更できない属性）の値からハッシュを計算してidentifierとしている。ただ、create-onlyプロパティが変更された場合、ハッシュが変わってidentifierも変わる。するとdifferは、<code>.crn</code>ファイルにある「新しいidentifierのリソース」とstateにある「古いidentifierのリソース」を同じリソースだと認識できず、「新しいリソースの作成」と「古いリソースの削除」という無関係な2つの操作として扱ってしまう。本来は「同じリソースの置き換え（Replace）」として認識させる必要がある。</p>
<p>そこで、<code>reconcile_anonymous_identifiers()</code>という仕組みを入れた（<a href="https://github.com/carina-rs/carina/issues/540">#540</a>）。create-onlyプロパティの一部が一致し一部が異なる場合、同じリソースの変更であると判定し、stateにある旧identifierを引き継ぐことでReplaceとして扱う。</p>
<p>さらに、create-onlyプロパティを持たないリソース（EC2 EIPなど）に対しては、全属性の<a href="https://en.wikipedia.org/wiki/SimHash">SimHash</a>（locality-sensitive hash）をidentifierとして使うようにした（<a href="https://github.com/carina-rs/carina/issues/592">#592</a>）。通常のハッシュは1ビットでも入力が変わると出力が大きく変わるが、SimHashは入力の類似度を保存する。属性を1つ変更しても数ビットしか変わらないので、Hamming距離で同一リソースかどうかを判定できる。ただし、tagsのようなMap属性をそのまま1つのfeature（ハッシュの入力単位）としてハッシュすると、tag1つの変更でも相対的な変化が大きくなりすぎて閾値を超えてしまう問題があった。Map/List値を個々のエントリに展開（flatten）してそれぞれ別のfeatureとすることで修正した（<a href="https://github.com/carina-rs/carina/issues/885">#885</a>）。</p>
<p>Terraformにはanonymous resourceの概念がないので、Carina独自の設計判断が多く、バグも出やすい領域だった。</p>
<hr />
<h2>Create-before-destroyの名前衝突回避とカスケード更新</h2>
<p>リソースのReplace（置き換え）を実行するcreate-before-destroyでも、Terraformにはない仕組みを入れた。</p>
<p>Terraformではcreate-before-destroyの際、名前の衝突は人間が<code>name_prefix</code>等で回避する必要がある。Carinaでは、リソースの<code>name_attribute</code>（S3なら<code>bucket_name</code>、IAMなら<code>role_name</code>）をスキーマから自動判定し、replaceの際に一時名を自動生成する（<a href="https://github.com/carina-rs/carina/issues/405">#405</a>）。名前がupdatable（create-onlyでない）なら、旧リソース削除後に本来の名前にリネームする。create-onlyな場合はリネームできないので一時名のまま残る。</p>
<p>依存リソースのカスケードアップデートも入れた（<a href="https://github.com/carina-rs/carina/issues/404">#404</a>）。create-before-destroyでは「新リソース作成→依存リソース更新→旧リソース削除」という順序で実行する必要があるが、この中間ステップの依存リソース更新をdifferが依存グラフから検出してplanに含める。</p>
<hr />
<h2>LSPの強化</h2>
<p>エディタ上での開発体験を良くするために、LSP（Language Server Protocol）まわりを大幅に改善した。</p>
<ul>
<li>プロバイダーブロックの補完（<a href="https://github.com/carina-rs/carina/issues/899">#899</a>）</li>
<li>リソース参照の型ベース補完（<a href="https://github.com/carina-rs/carina/issues/914">#914</a>） — <code>route_table_id</code>に対してRouteTableを返すリソースだけを候補に出す</li>
<li>read-only属性をリソースブロック内の補完候補から除外（<a href="https://github.com/carina-rs/carina/issues/921">#921</a>）</li>
<li>属性ホバーで間違った説明を表示していた問題の修正（<a href="https://github.com/carina-rs/carina/issues/924">#924</a>）</li>
<li>未定義参照の検出強化（<a href="https://github.com/carina-rs/carina/issues/871">#871</a>）</li>
<li>重複letバインディングの検出（<a href="https://github.com/carina-rs/carina/issues/916">#916</a>）</li>
<li>未使用letバインディングの警告（<a href="https://github.com/carina-rs/carina/issues/530">#530</a>）</li>
<li>ネストされたstructブロックの任意深さでの補完・診断（<a href="https://github.com/carina-rs/carina/issues/400">#400</a>、<a href="https://github.com/carina-rs/carina/issues/402">#402</a>）</li>
<li>非ASCII文字によるbyte/charインデックスのずれの修正（<a href="https://github.com/carina-rs/carina/issues/666">#666</a>）</li>
</ul>
<hr />
<h2>コード生成の強化</h2>
<p>CloudFormationスキーマとSmithyモデルからのコード生成器（codegen）に、バリデーション制約のサポートを追加した。文字列長（minLength/maxLength）、配列長（minItems/maxItems）、正規表現パターン、フォーマット制約（int64、URIなど）、デフォルト値、複数制約の合成（pattern + lengthなど）をサポートしている（<a href="https://github.com/carina-rs/carina/issues/628">#628</a>〜<a href="https://github.com/carina-rs/carina/issues/636">#636</a>）。</p>
<p>これにより、<code>carina validate</code>で実際にAWSにリクエストを投げる前に、かなり細かいバリデーションができるようになった。</p>
<hr />
<h2>State管理の改善</h2>
<p>本番運用を見据えたstate管理の改善も進めた。</p>
<ul>
<li>state lockの改善 — TTLを60分に延長し、lock renewalとconditional writeを追加（<a href="https://github.com/carina-rs/carina/issues/857">#857</a>）</li>
<li>アトミックなstate書き込み — ローカルバックエンドのstate書き込みをatomicに（<a href="https://github.com/carina-rs/carina/issues/849">#849</a>）</li>
<li><code>--lock=false</code>オプション — state lockのスキップ（<a href="https://github.com/carina-rs/carina/issues/893">#893</a>）</li>
<li>orphanedリソースの検出 — <code>.crn</code>ファイルから消えたリソースの自動検出・削除（<a href="https://github.com/carina-rs/carina/issues/853">#853</a>）</li>
<li>state refreshコマンド — AWSの実際の状態とstateの同期（<a href="https://github.com/carina-rs/carina/issues/395">#395</a>）</li>
<li>エラーパスでのlock解放保証（<a href="https://github.com/carina-rs/carina/issues/779">#779</a>）</li>
<li>プロバイダースコープのstate identity — マルチプロバイダー構成でのstate破損防止（<a href="https://github.com/carina-rs/carina/issues/854">#854</a>）</li>
</ul>
<hr />
<h2>その他の新機能</h2>
<ul>
<li><strong>フォーマッター</strong> — list-of-mapsをblock構文に変換（<a href="https://github.com/carina-rs/carina/issues/911">#911</a>）</li>
<li><strong>block構文</strong> — <code>List&lt;Struct&gt;</code>属性に対するブロック構文のサポートと、codegenでの自動block_name生成（<a href="https://github.com/carina-rs/carina/issues/381">#381</a>、<a href="https://github.com/carina-rs/carina/issues/759">#759</a>）</li>
<li><strong>前方参照</strong> — 宣言順序に依存しないパーサー（<a href="https://github.com/carina-rs/carina/issues/876">#876</a>）</li>
<li><strong>循環依存検出</strong> — リソース間の循環依存をエラーとして報告（<a href="https://github.com/carina-rs/carina/issues/665">#665</a>）</li>
<li><strong>型認識diff</strong> — differが型情報を持って意味的な比較を行うように（<a href="https://github.com/carina-rs/carina/issues/613">#613</a>）</li>
<li><strong>ordered/unordered list</strong> — AWSCCスキーマのリストに順序付き/順序なしのセマンティクスを追加（<a href="https://github.com/carina-rs/carina/issues/873">#873</a>）</li>
</ul>
<hr />
<h2>リファクタリング</h2>
<p>コードベースが大きくなってきたので、構造的な整理も進めた。main.rs（4000行）、awscc provider.rs（3500行）、differ.rs、diagnostics.rs、completion.rsなどの大きなファイルを分割し、carina-coreからvalidation、resolver、deps、config_loaderなどのモジュールを抽出した（<a href="https://github.com/carina-rs/carina/issues/382">#382</a>〜<a href="https://github.com/carina-rs/carina/issues/392">#392</a>、<a href="https://github.com/carina-rs/carina/issues/673">#673</a>、<a href="https://github.com/carina-rs/carina/issues/787">#787</a>）。</p>
<p>また、CLIやLSPのコードに<code>&quot;aws&quot;</code>や<code>&quot;awscc&quot;</code>といったプロバイダー名が直接書かれていたのを、<code>ProviderFactory</code>トレイト経由で動的に解決するように変更した（<a href="https://github.com/carina-rs/carina/issues/364">#364</a>、<a href="https://github.com/carina-rs/carina/issues/367">#367</a>）。新しいプロバイダーを追加してもCLIやLSPのコードを変更する必要がなくなった。</p>

]]></content>
    </entry>

    <entry>
        <title type="html"><![CDATA[Claude CodeによるCarinaのイシュー駆動開発]]></title>
        <link href="https://mizzy.org/blog/2026/02/21/1/"/>
        <updated>2026-02-21T17:11:50+09:00</updated>
        <id>https://mizzy.org/blog/2026/02/21/1/</id>
        <content type="html"><![CDATA[
<p><a href="https://github.com/carina-rs/carina">Carina</a>の開発はこれまでコード自体をほとんど見ずにやらせていたので、コード全体の品質や一貫性がどうなっているかは把握していなかった。</p>
<p>そこで、Claude Codeに問題点をGitHubイシューとして登録させ、そのイシューをひとつずつ潰させる、というサイクルを回してみた。イシューの対応はすべて自分でPRをレビューしてからマージしていて、これは自分自身のコードや設計の理解を深める意味もある。10日間回した結果、90個のイシューが登録され、67個がクローズされた。</p>
<hr />
<h2>作業フロー</h2>
<p>具体的な作業フローはこんな感じ。</p>
<ol>
<li><code>/pick-issue</code>スキルを呼ぶと、一番新しいオープンイシューをpickしてくる</li>
<li>Claude Codeがイシューを修正してPRを出す</li>
<li><code>/self-review</code>スキルで5回セルフレビューさせる</li>
<li>セルフレビューの過程でPRに関連する問題が見つかればその場で修正、無関係な問題であれば新たにイシューを登録</li>
<li>自分がPRをレビューしてマージ</li>
<li>1に戻る</li>
</ol>
<p>イシューの優先順位付けは特にやっていない。<code>/pick-issue</code>が一番新しいイシューを拾ってくるので、基本的にはFILO（後入れ先出し）で回している。気になるイシューがあれば<code>/pick-issue 123</code>のように番号指定することもあるが、大半はそのまま最新のものを拾わせている。修正の過程で見つかった新しいイシューが次にpickされやすいので、関連する問題が芋づる式に片付いていく、という副次的な効果もある。</p>
<p>イシューの登録元はセルフレビューだけではなく、コードベース全体のレビューや、acceptance testの実行結果から問題を拾ってイシューに登録させる、ということもやっている。</p>
<p>このフローだと、完全にボトルネックは人間によるレビュー。レビュー対象はPRのコードだけでなく、Claude Codeが立てたplanも含まれる。plan段階でプロジェクトの方針とまったく逆方向の提案をしてくることもあるので、方針がおかしければそこで差し戻す。</p>
<hr />
<h2>Day 1: 23個のイシュー</h2>
<p>まずClaude Codeにコードベース全体のレビューとacceptance testの実行をさせたところ、1日で23個のイシューが登録された。バグ、設計上の問題、不足している機能など、色々なカテゴリのイシューが一気に出てきた。たとえば:</p>
<ul>
<li><code>Effect::Delete</code>にリソース識別子が含まれていない（<a href="https://github.com/carina-rs/carina/issues/135">#135</a>）</li>
<li>IGWのデタッチエラーが握りつぶされている（<a href="https://github.com/carina-rs/carina/issues/136">#136</a>）</li>
<li><code>create_plan()</code>が削除すべきリソースを検出しない（<a href="https://github.com/carina-rs/carina/issues/137">#137</a>）</li>
<li>Structバリデーションがunknown fieldを無視している（<a href="https://github.com/carina-rs/carina/issues/138">#138</a>）</li>
<li>plan fileの保存とapplyが欲しい（<a href="https://github.com/carina-rs/carina/issues/142">#142</a>）</li>
<li><code>name</code>属性をリソース識別子として使うのをやめるべき（<a href="https://github.com/carina-rs/carina/issues/148">#148</a>）</li>
<li>S3バケットの中身が空でないと削除できない（<a href="https://github.com/carina-rs/carina/issues/160">#160</a>）</li>
</ul>
<hr />
<h2>Day 2-3: 修正がさらなる問題を呼ぶ</h2>
<p>初日のイシューを潰し始めると、修正の過程で新たな問題が次々と見つかった。</p>
<p>特に大きかったのが、<code>--detailed-exitcode</code>（<a href="https://github.com/carina-rs/carina/issues/130">#130</a>）の実装と、acceptance testへのapply後plan verify追加（<a href="https://github.com/carina-rs/carina/issues/129">#129</a>）。apply後にもう一度planを実行して差分がないことを確認する仕組みを入れたところ、べき等性の問題が大量に見つかった。</p>
<ul>
<li>enum値の大文字小文字が正規化されない（<a href="https://github.com/carina-rs/carina/issues/174">#174</a>、<a href="https://github.com/carina-rs/carina/issues/176">#176</a>）</li>
<li>AWSから返ってきた値にスキーマにない余分なフィールドが含まれている（<a href="https://github.com/carina-rs/carina/issues/172">#172</a>）</li>
<li><code>List&lt;Struct&gt;</code>の比較が順序依存で、false diffが出る（<a href="https://github.com/carina-rs/carina/issues/171">#171</a>）</li>
</ul>
<p>acceptance testを実際に10アカウント並列で回すと、テストランナー自体のバグも出てきた。ワーカーサブシェルがエラーで黙って停止する問題（<a href="https://github.com/carina-rs/carina/issues/170">#170</a>）や、apply失敗時のリソースクリーンアップ（<a href="https://github.com/carina-rs/carina/issues/180">#180</a>）など。こういった問題もすべてイシューとして登録させて、同じフローで潰していった。</p>
<hr />
<h2>Day 4-6: enumの大掃除</h2>
<p>べき等性の問題を追いかけていくと、根本原因がenum処理の一貫性のなさに行き着いた。</p>
<p>enumの正規化・バリデーション・変換まわりはアドホックなコードが多く、<code>normalize_instance_tenancy</code>、<code>normalize_region</code>のような個別のハードコードされた関数が各crateに散らばっていて、それぞれ微妙に違う挙動をしていた。</p>
<p>ここからenumの大掃除が始まった。</p>
<ul>
<li>複数crateに散らばっていた<code>normalize_*</code>関数を<code>carina-core::utils</code>に集約（<a href="https://github.com/carina-rs/carina/issues/201">#201</a>、<a href="https://github.com/carina-rs/carina/issues/204">#204</a>）</li>
<li><code>get_enum_valid_values()</code>をスキーマ定義から自動生成（<a href="https://github.com/carina-rs/carina/issues/186">#186</a>）</li>
<li>大文字小文字を区別しないenum照合（<a href="https://github.com/carina-rs/carina/issues/217">#217</a>）</li>
<li><code>extract_enum_value</code>パターンの重複排除（<a href="https://github.com/carina-rs/carina/issues/203">#203</a>、<a href="https://github.com/carina-rs/carina/issues/209">#209</a>、<a href="https://github.com/carina-rs/carina/issues/219">#219</a>）</li>
<li>ハードコードされた特殊処理の削除（<a href="https://github.com/carina-rs/carina/issues/199">#199</a>）</li>
</ul>
<p>ひとつ直すと「この関数と同じパターンがあっちにもある」「この関数、もう使われてないのでは」と芋づる式にイシューが出てくる。Claude Codeに「さっき直した関数と同じパターンが他にもないか探して」と指示すると、重複を見つけてイシューを登録し、修正してくれる。</p>
<hr />
<h2>Day 7: APIの引き締め</h2>
<p>enumの大掃除が一段落したところで、今度はAPIの公開範囲の見直しに入った。</p>
<ul>
<li><code>validate_iam_policy_document</code>をprivateに（<a href="https://github.com/carina-rs/carina/issues/231">#231</a>）</li>
<li><code>validate_namespaced_enum</code>を<code>pub(crate)</code>に（<a href="https://github.com/carina-rs/carina/issues/233">#233</a>）</li>
<li>type factory関数を<code>pub(crate)</code>に（<a href="https://github.com/carina-rs/carina/issues/236">#236</a>）</li>
<li><code>validate_*</code>関数のエラーメッセージ形式を統一（<a href="https://github.com/carina-rs/carina/issues/228">#228</a>）</li>
</ul>
<p>この日は13個のイシューが登録され、13個すべてクローズ。登録してすぐ直す、のリズムが回っていた。</p>
<p>同じ日に、コード生成まわりのバグもいくつか見つかった。VPC Endpoint IDの演算子優先順位問題（<a href="https://github.com/carina-rs/carina/issues/244">#244</a>）は、初日に登録されていた<a href="https://github.com/carina-rs/carina/issues/132">#132</a>の派生バグで、5日かけて全箇所を潰した形になる。</p>
<hr />
<h2>Day 8-9: CLIからプロバイダ固有ロジックを追い出す</h2>
<p>初日のレビューで「carina-coreやCLIにプロバイダ固有のロジックが入り込んでいる」（<a href="https://github.com/carina-rs/carina/issues/155">#155</a>）というイシューが登録されていた。ここに本格的に着手。</p>
<p><code>ProviderFactory</code>トレイトを導入して、設定バリデーション、リージョン抽出、プロバイダインスタンス化、スキーマ読み込みを抽象化した（<a href="https://github.com/carina-rs/carina/issues/259">#259</a>）。CLI側にあった<code>&quot;aws&quot;</code>や<code>&quot;awscc&quot;</code>のmatchブロックがなくなり、新しいプロバイダを追加してもCLIのコードを変更する必要がなくなった。</p>
<p>enum処理のうちプロバイダ固有のものも、CLI側からProvider trait側に移動させた（<a href="https://github.com/carina-rs/carina/issues/185">#185</a>、<a href="https://github.com/carina-rs/carina/issues/190">#190</a>）。</p>
<hr />
<h2>全体を振り返って</h2>
<p>10日間のサイクルで実装された主な機能・改善をまとめておく。</p>
<p><strong>新機能:</strong></p>
<ul>
<li>plan fileの保存とapply（<code>carina plan --out=plan.json</code> / <code>carina apply plan.json</code>）</li>
<li>孤児リソースの自動検出・削除</li>
<li><code>--detailed-exitcode</code>フラグ</li>
<li><code>lifecycle { force_delete }</code>メタ引数</li>
<li><code>&lt;attr&gt;_prefix</code>サポート（ランダムサフィックス付きリソース名生成）</li>
<li>unknown fieldのLevenshtein距離によるサジェスト</li>
<li>enumエイリアス（<code>IpProtocol</code>の<code>all</code> → <code>-1</code>など）</li>
</ul>
<p><strong>リファクタリング:</strong></p>
<ul>
<li>リソース識別子の再設計（<code>name</code>属性からの分離）</li>
<li>enumシステムの全面書き直し（17 PR）</li>
<li>型の厳密化（String → 具体的なARN型、リソースID型、enum型など）</li>
<li><code>ProviderFactory</code> traitによるCLI/プロバイダの分離</li>
<li>API公開範囲の引き締め</li>
</ul>
<p><strong>テスト:</strong></p>
<ul>
<li>テスト数: 250 → 454</li>
<li>apply後のplan verifyによるべき等性チェック</li>
<li>テストランナーのシグナルハンドリング、事前認証、クリーンアップ改善</li>
</ul>

]]></content>
    </entry>

</feed>
