はったりエンジニアの備忘録
2023-11-12T15:50:00+09:00
Manabu Sakai
https://blog.manabusakai.com/
GitHub-hosted larger runners の CPU モデルを調べてみた
2023-11-12T15:50:00+09:00
https://blog.manabusakai.com/2023/11/cpu-model-of-larger-runners/cpu-model-of-larger-runners
2 vCPUs の standard runners は Intel Xeon プロセッサ、4 vCPUs 以上の larger runners は AMD EPYC プロセッサが使われていました。
<p>GitHub Actions で実行しているとあるテストに時間がかかるようになってきました。そのテストはマシンパワーを必要とするので、テストのカバレッジが上がるほど時間がかかる状態でした。</p>
<p>そこで今年 6 月に GA した GitHub-hosted larger runners を試してみることにしました。</p>
<ul>
<li><a href="https://github.blog/changelog/2023-06-21-github-hosted-larger-runners-for-actions-are-generally-available/">GitHub-hosted larger runners for Actions are generally available - The GitHub Blog</a></li>
</ul>
<p>larger runners を有効にすれば最大 64 vCPUs で実行することができるため、マルチコアを活かせるテストは並列度を上げることができます。</p>
<p>料金は単純明快で、vCPUs が倍になると料金も倍になります(以下は Linux の場合)。もし vCPUs を倍にして時間が半分になるのであれば、必要なコストは同じになります。</p>
<table>
<thead>
<tr>
<th>vCPUs</th>
<th>Per-minute rate</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>$0.008</td>
</tr>
<tr>
<td>4</td>
<td>$0.016</td>
</tr>
<tr>
<td>8</td>
<td>$0.032</td>
</tr>
<tr>
<td>16</td>
<td>$0.064</td>
</tr>
<tr>
<td>32</td>
<td>$0.128</td>
</tr>
<tr>
<td>64</td>
<td>$0.256</td>
</tr>
</tbody>
</table>
<h3>GitHub-hosted larger runners は CPU モデルが違う</h3>
<p>larger runners で何度かテストを実行したところ、マルチコアを使えるように調整せずとも速くなっていることに気が付きました(standard runners の 2 vCPUs で並列度を上げると不安定になるので、明示的に 1 並列で実行するようにしていました)。</p>
<p>気になったので次のようなワークフローで 2 vCPUs 〜 64 vCPUs の <code class="language-plaintext highlighter-rouge">/proc/cpuinfo</code> を確認してみました。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Check cpuinfo</span>
<span class="na">on</span><span class="pi">:</span> <span class="s">push</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="na">ubuntu-latest</span><span class="pi">:</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">cat /proc/cpuinfo</span>
<span class="na">ubuntu-latest-4core</span><span class="pi">:</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest-4core</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">cat /proc/cpuinfo</span>
<span class="na">ubuntu-latest-8core</span><span class="pi">:</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest-8core</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">cat /proc/cpuinfo</span>
<span class="na">ubuntu-latest-16core</span><span class="pi">:</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest-16core</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">cat /proc/cpuinfo</span>
<span class="na">ubuntu-latest-32core</span><span class="pi">:</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest-32core</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">cat /proc/cpuinfo</span>
<span class="na">ubuntu-latest-64core</span><span class="pi">:</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest-64core</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">cat /proc/cpuinfo</span>
</code></pre></div></div>
<p>結果は次のとおりです。 2 vCPUs の standard runners は Intel Xeon プロセッサ、4 vCPUs 以上の larger runners は AMD EPYC プロセッサが使われていました。</p>
<table>
<thead>
<tr>
<th>vCPUs</th>
<th>CPU model</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>Intel(R) Xeon(R) Platinum 8272CL CPU @ 2.60GHz</td>
</tr>
<tr>
<td>4</td>
<td>AMD EPYC 7763 64-Core Processor</td>
</tr>
<tr>
<td>8</td>
<td>AMD EPYC 7763 64-Core Processor</td>
</tr>
<tr>
<td>16</td>
<td>AMD EPYC 7763 64-Core Processor</td>
</tr>
<tr>
<td>32</td>
<td>AMD EPYC 7763 64-Core Processor</td>
</tr>
<tr>
<td>64</td>
<td>AMD EPYC 7763 64-Core Processor</td>
</tr>
</tbody>
</table>
<p>Public IP から推測すると、GitHub Actions はおそらく Azure VM で動いているはずです(GitHub は 2018 年に Microsoft に買収されていますし)。</p>
<p>Azure VM には上記の CPU を採用したシリーズは複数ありますが、CPU の性能(特に 1 コアあたりの性能)が向上していることは間違いなさそうです。仮に D シリーズだとすると、v4 と v5 の違いがありそうです。</p>
<ul>
<li><a href="https://azure.microsoft.com/ja-jp/pricing/details/virtual-machines/series/">Virtual Machines シリーズ | Microsoft Azure</a></li>
</ul>
<blockquote>
<p>Dv4 および Ddv4 仮想マシンは、カスタム Intel® Xeon® Platinum 8272CL プロセッサに基づいています。</p>
</blockquote>
<blockquote>
<p>Dasv5 および Dadsv5 シリーズの仮想マシンは、第 3 世代の AMD EPYC™ 7763v (Premium) プロセッサに基づいています。</p>
</blockquote>
<p>AMD EPYC 7763 は物理コア数が 64 なので、Azuru VM で新しい世代がリリースされない限りは 64 vCPUs 以上の larger runners は登場しないと思われます。</p>
<h3>まとめ</h3>
<p>上にも書いたとおり vCPUs と料金は比例する関係にあるので、マルチコアを使ってテストを並列化できる場合は larger runners を使ったほうがコストメリットがありそうです。</p>
<p>ただし、 larger runners はプランに含まれる無料分には含まれないので注意してください(たとえば Team プランだと 3,000 minutes/month の無料枠が付いていますが、larger runners はここには含まれません)。</p>
Amazon ECS の古いタスク定義を断捨離する "tdtidy" という隙間家具 OSS を作った
2023-10-31T08:55:00+09:00
https://blog.manabusakai.com/2023/10/tdtidy/tdtidy
Containers Roadmap の Issue を参考にして、Amazon ECS の古いタスク定義を断捨離する "tdtidy" という隙間家具 OSS を作りました。
<p>自分は Amazon ECS のデプロイに <a href="https://github.com/kayac/ecspresso/">ecspresso</a> を利用することが多いのですが、頻繁にデプロイする環境だと 1 年で数百を超えるタスク定義が作られます。直近の数世代はロールバックする可能性があるので残しておきたいのですが、さすがに数か月前のリビジョンに戻すことはないため不要なものは断捨離したいと思っていました。</p>
<p>そういうリクエストが多かったのか、以前はタスク定義を非アクティブにすることしかできませんでしたが、今年の 2 月についに削除できるようになりました。</p>
<ul>
<li><a href="https://aws.amazon.com/jp/about-aws/whats-new/2023/02/amazon-ecs-deletion-inactive-task-definition-revisions/">Amazon ECS が非アクティブなタスク定義リビジョンの削除をサポート</a></li>
</ul>
<p>さらに AWS が管理する Containers Roadmap のプロジェクトを眺めてみると、<a href="https://github.com/aws/containers-roadmap/issues/1967">Issue #1967</a> でタスク定義にライフサイクルを追加してほしいというリクエストが挙がっていました(この Issue を作ったのは中の人っぽいので、要望をリサーチするためのものかもしれません)。 ECR のライフサイクルポリシーに近しい機能のようです。</p>
<p>現時点であまり動きはなくいつリリースされるかも未知数なので、古いタスク定義を断捨離する "tdtidy" という隙間家具 OSS を作りました(隙間家具については後述します)。</p>
<ul>
<li><a href="https://github.com/manabusakai/tdtidy">manabusakai/tdtidy: A command line tool for managing ECS task definitions. <code class="language-plaintext highlighter-rouge">tdtidy</code> can deregister and delete old task definitions.</a></li>
</ul>
<p>名前の由来は "task definitions tidy" の略で <code class="language-plaintext highlighter-rouge">go mod tidy</code> から着想を得ています。</p>
<h3>インストールと使い方</h3>
<p>今回は Go で書いたツールを OSS で公開するための方法も学びたかったので、Homebrew でインストールできるようにしています。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ brew install manabusakai/tap/tdtidy
$ tdtidy -help
Usage of tdtidy:
-dry-run
Turn on dry-run. List the target task definitions.
-family-prefix string
Family name of task definitions. If specified, filter by family name.
-retention-period int
Retention period for task definitions. Unit is number of days. The default value is zero.
</code></pre></div></div>
<p>使い方のイメージですが、90 日より前に作られた foobar というタスク定義のリビジョンを断捨離したい場合は次のようなコマンドを実行します(対象を確認したい場合は <code class="language-plaintext highlighter-rouge">-dry-run</code> フラグを付けます)。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ tdtidy -family-prefix foobar -retention-period 90
</code></pre></div></div>
<p>ここでいう断捨離とは、アクティブなリビジョンを非アクティブにすること (deregister)、非アクティブなリビジョンを削除すること (delete) を指しています。なお、実行中のタスクに紐づいているかどうかはチェックしていませんが、もし紐づいていたとしてもリビジョンのステータスが <code class="language-plaintext highlighter-rouge">DELETE_IN_PROGRESS</code> に留まるので実行中のタスクには影響ありません。</p>
<p>手元から実行しても良いのですが、Lambda で定期的に実行するか、デプロイパイプラインの最後に実行することを想定しています。</p>
<h3>隙間家具 OSS とは</h3>
<p>隙間家具 OSS というのは ecspresso の作者である <a href="https://twitter.com/fujiwara">@fujiwara</a> さんが使われている言葉で、マネージドサービスの隙間を埋めて便利にするものを指しています。今回の tdtidy もいずれマネージドサービスとしてリリースされると不要になることを念頭において作りました。</p>
<script defer="" class="speakerdeck-embed" data-id="b195abe54742459a865d27764e3adbd5" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js"></script>
<p>リスペクトの意味を込めて、この言葉を使わせていただきました!</p>
MySQL のオンライン DDL は複数の条件が関係する
2023-10-15T16:40:00+09:00
https://blog.manabusakai.com/2023/10/online-ddl-conditions/online-ddl-conditions
オンライン DDL で実行されることを期待したカラム追加のクエリが、想定通りに実行されず障害になりかけたので原因を調べてみました。
<p>オンライン DDL で実行されることを期待したカラム追加のクエリが、想定通りに実行されず障害になりかけたので原因を調べてみました。結論から言うと公式ドキュメントに書いてある通りだったのですが、複数の条件がオンライン DDL に関係するとは思っていなかったので学びがありました。</p>
<h3>検証環境</h3>
<p>MySQL 8.0.34 の Docker イメージを使って検証を行いました。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ docker network create mysql
$ docker run --name mysql-8.0 --network mysql -d -e MYSQL_ROOT_PASSWORD=root mysql:8.0.34
$ docker run -it --rm --network mysql mysql:8.0.34 mysql -hmysql-8.0 -uroot -p
mysql> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 8.0.34 |
+-----------+
1 row in set (0.00 sec)
</code></pre></div></div>
<h3>オンライン DDL でカラムを追加する</h3>
<p>検証に使うテーブルを作成します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mysql> CREATE TABLE users (first_name VARCHAR(10), last_name VARCHAR(10), email VARCHAR(32));
Query OK, 0 rows affected (0.02 sec)
mysql> DESC users;
+------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| first_name | varchar(10) | YES | | NULL | |
| last_name | varchar(10) | YES | | NULL | |
| email | varchar(32) | YES | | NULL | |
+------------+-------------+------+-----+---------+-------+
3 rows in set (0.01 sec)
</code></pre></div></div>
<p>適当なカラムを追加します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mysql> ALTER TABLE users ADD COLUMN birth DATE, ALGORITHM=INPLACE, LOCK=NONE;
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> DESC users;
+------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| first_name | varchar(10) | YES | | NULL | |
| last_name | varchar(10) | YES | | NULL | |
| email | varchar(32) | YES | | NULL | |
| birth | date | YES | | NULL | |
+------------+-------------+------+-----+---------+-------+
4 rows in set (0.00 sec)
</code></pre></div></div>
<p>オンライン DDL で実行したいので、明示的に <code class="language-plaintext highlighter-rouge">ALGORITHM=INPLACE</code> と <code class="language-plaintext highlighter-rouge">LOCK=NONE</code> を付けています。このように期待する動作を明示することで、オンライン DDL で実行できない場合はエラーになってくれます。</p>
<p>公式ドキュメントの "<a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html#online-ddl-column-operations">Column Operations</a>" を確認すると、カラム追加は "In Place" と "Permits Concurrent DML" がどちらも Yes になっているので想定通りです。</p>
<h3>複数条件でオンライン DDL が失敗するケース</h3>
<p>先ほどのテーブルに <code class="language-plaintext highlighter-rouge">first_name</code> と <code class="language-plaintext highlighter-rouge">last_name</code> を結合した <code class="language-plaintext highlighter-rouge">full_name</code> を Generated Column で定義します。 <a href="https://dev.mysql.com/doc/refman/8.0/en/create-table-generated-columns.html">Generated Column</a> は定義した式に従って値を生成することができるカラムです。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mysql> ALTER TABLE users ADD COLUMN full_name VARCHAR(32) GENERATED ALWAYS AS (CONCAT(first_name ,' ', last_name)) VIRTUAL AFTER last_name, ALGORITHM=INPLACE, LOCK=NONE;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> DESC users;
+------------+-------------+------+-----+---------+-------------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------------------+
| first_name | varchar(10) | YES | | NULL | |
| last_name | varchar(10) | YES | | NULL | |
| full_name | varchar(32) | YES | | NULL | VIRTUAL GENERATED |
| email | varchar(32) | YES | | NULL | |
| birth | date | YES | | NULL | |
+------------+-------------+------+-----+---------+-------------------+
5 rows in set (0.01 sec)
</code></pre></div></div>
<p>この状態で <code class="language-plaintext highlighter-rouge">full_name</code> より前にカラム追加しようとすると失敗します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mysql> ALTER TABLE users ADD COLUMN id INT FIRST, ALGORITHM=INPLACE, LOCK=NONE;
ERROR 1846 (0A000): ALGORITHM=INPLACE is not supported. Reason: INPLACE ADD or DROP of virtual columns cannot be combined with other ALTER TABLE actions. Try ALGORITHM=COPY.
</code></pre></div></div>
<p>先ほど試したようにカラム追加はオンライン DDL で実行できるはずですが、なぜ失敗するのでしょうか?</p>
<p>エラーメッセージにも出ていますが、"<a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html#online-ddl-generated-column-operations">Generated Column Operations</a>" を参照すると Generated Column の順序入れ替えは "In Place" と "Permits Concurrent DML" が No になっています。この例では <code class="language-plaintext highlighter-rouge">id</code> を先頭に追加しようとしたことで Generated Column の <code class="language-plaintext highlighter-rouge">full_name</code> の位置がズレることになり、結果的にオンライン DDL では実行できなかったのです。</p>
<p>明示的に <code class="language-plaintext highlighter-rouge">ALGORITHM=INPLACE</code> と <code class="language-plaintext highlighter-rouge">LOCK=NONE</code> を付けたのでエラーになりましたが、付けずに実行した場合は <code class="language-plaintext highlighter-rouge">ALGORITHM=COPY</code> のテーブルコピーになります。テーブルコピーで実行されるとテーブルへの更新がブロックされるため、オンライン DDL が有効だろうと思って実行した場合はセッションが詰まって障害になるかもしれません。</p>
<p>冒頭に書いた障害になりかけたケースでは、運悪く先ほどの条件が揃ってしまいました。</p>
<ul>
<li>Generated Column の存在を認識していなかった</li>
<li><code class="language-plaintext highlighter-rouge">ALTER TABLE</code> でカラムを追加しようとしたが、位置を指定したため Generated Column の順序入れ替えが発生した</li>
<li>オンライン DDL を期待していたが <code class="language-plaintext highlighter-rouge">ALGORITHM</code> 句と <code class="language-plaintext highlighter-rouge">LOCK</code> 句を付けていなかった</li>
</ul>
<h3>まとめ</h3>
<p>今回は Generated Column を例に取りましたが、ほかの組み合わせでも起こりえるかもしれません。事前にドキュメントを確認するのはもちろんのこと、ステージング環境などで <code class="language-plaintext highlighter-rouge">ALGORITHM</code> 句と <code class="language-plaintext highlighter-rouge">LOCK</code> 句を付けて想定している動作になるか確認するのが大切ですね。</p>
Athena で GetParameter / GetParameters の回数を集計する
2023-09-27T12:40:00+09:00
https://blog.manabusakai.com/2023/09/aggregate-the-number-of-getparameter-and-getparameters-with-athena/aggregate-the-number-of-getparameter-and-getparameters-with-athena
Athena で指定した期間内に GetParameter / GetParameters が何回呼ばれたのかを集計します。
<p>AWS Systems Manager の Parameter Store は、最後に変更した日付とユーザーは API で確認できますが、最後に取得された日付はわかりません。 CloudTrail で <code class="language-plaintext highlighter-rouge">GetParameter</code> / <code class="language-plaintext highlighter-rouge">GetParameters</code> のイベント名で検索すれば調べられますが、使われていないものを探し出すのは大変です。</p>
<p>『<a href="https://dev.classmethod.jp/articles/who-uses-this-parameter-store/">「このパラメータストア、誰が使ってるん?」の疑問を解消するための CloudTrail と Athena の使い方</a>』のように個別に調べていくことはできますが、数が多いと手間がかかるためもう少し楽な方法を考えてみました。</p>
<p>次の SQL は指定した期間内に <code class="language-plaintext highlighter-rouge">GetParameter</code> / <code class="language-plaintext highlighter-rouge">GetParameters</code> が何回呼ばれたのかを集計します。</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
<span class="n">name</span><span class="p">,</span>
<span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">apiCount</span>
<span class="k">FROM</span>
<span class="p">(</span>
<span class="k">SELECT</span>
<span class="n">name</span>
<span class="k">FROM</span>
<span class="k">table_name</span> <span class="c1">-- FIXME</span>
<span class="k">CROSS</span> <span class="k">JOIN</span>
<span class="k">UNNEST</span><span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="n">json_extract</span><span class="p">(</span><span class="n">requestParameters</span><span class="p">,</span> <span class="s1">'$.names'</span><span class="p">)</span> <span class="k">AS</span> <span class="n">array</span><span class="p">(</span><span class="nb">varchar</span><span class="p">)))</span> <span class="k">AS</span> <span class="n">t</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
<span class="k">WHERE</span>
<span class="n">eventName</span> <span class="o">=</span> <span class="s1">'GetParameters'</span>
<span class="k">AND</span>
<span class="n">eventTime</span> <span class="o">>=</span> <span class="s1">'2023-09-01 00:00:00'</span>
<span class="k">UNION</span> <span class="k">ALL</span>
<span class="k">SELECT</span>
<span class="k">CAST</span><span class="p">(</span><span class="n">json_extract</span><span class="p">(</span><span class="n">requestParameters</span><span class="p">,</span> <span class="s1">'$.name'</span><span class="p">)</span> <span class="k">AS</span> <span class="nb">varchar</span><span class="p">)</span> <span class="k">AS</span> <span class="n">name</span>
<span class="k">FROM</span>
<span class="k">table_name</span> <span class="c1">-- FIXME</span>
<span class="k">WHERE</span>
<span class="n">eventName</span> <span class="o">=</span> <span class="s1">'GetParameter'</span>
<span class="k">AND</span>
<span class="n">eventTime</span> <span class="o">>=</span> <span class="s1">'2023-09-01 00:00:00'</span>
<span class="p">)</span>
<span class="k">GROUP</span> <span class="k">BY</span>
<span class="n">name</span>
<span class="k">ORDER</span> <span class="k">BY</span>
<span class="n">apiCount</span> <span class="k">DESC</span>
</code></pre></div></div>
<p>実行結果は次のようなイメージです。ここに挙がってこないものは使われていないと判断できます。</p>
<p><img src="/image/upload/2023/athena-results.png" alt="Athena の実行結果" /></p>
<p>Parameter Store を取得する API は <code class="language-plaintext highlighter-rouge">GetParameter</code> と <code class="language-plaintext highlighter-rouge">GetParameters</code> の 2 種類があるため、サブクエリで 2 つの結果を <code class="language-plaintext highlighter-rouge">UNION ALL</code> で結合しています。</p>
<p><code class="language-plaintext highlighter-rouge">GetParameters</code> の結果は以下のような配列になっているため、フラット化するために <code class="language-plaintext highlighter-rouge">CROSS JOIN</code> と <code class="language-plaintext highlighter-rouge">UNNEST</code> で配列をバラして行に展開しています。</p>
<ul>
<li><a href="https://docs.aws.amazon.com/ja_jp/athena/latest/ug/flattening-arrays.html">ネストされた配列のフラット化 - Amazon Athena</a></li>
</ul>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"names"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"/foo/bar"</span><span class="p">,</span><span class="w">
</span><span class="s2">"/hoge/fuga"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"withDecryption"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>ちなみに、CloudTrail Lake でも同じようなことはできますが、CloudTrail Lake SQL と Athena SQL では細かい点で違いがあり、複雑な集計をしようとすると結構ハマります(上の SQL も CloudTrail Lake ではエラーになります)。 Athena の知見を活かせないので、個人的には CloudTrail Lake を積極的に使いたいとは感じませんでした。</p>
AWS Certified Security - Specialty を受験してきた
2023-07-30T23:30:00+09:00
https://blog.manabusakai.com/2023/07/aws-certified-security-specialty/aws-certified-security-specialty
先日 AWS Certified Security - Specialty (SCS-C02) を受験して無事に合格しました。
<p>先日 AWS Certified Security - Specialty (SCS-C02) を受験して無事に合格しました。もともとアソシエイトとプロフェッショナルの 5 つは持っていましたが、スペシャリティの認定は初めて受験しました。</p>
<p><img src="/image/upload/2023/scs-c02.png" alt="AWS Certified Security - Specialty の受験結果" /></p>
<p>ちなみに、AWS Certified Security - Specialty は 7 月 11 日に改定されて SCS-C02 になったばかりです。</p>
<h3>モチベーションと勉強方法</h3>
<p>仕事やプライベートで使っているサービスは普段から意識的にキャッチアップしていますが、セキュリティ関連のサービスはあまり触れたことがなく、中には名前しか知らないサービスもありました。よく使っているサービスでも、新しく追加された機能をキャッチアップできていないこともありました。</p>
<p>なので、今回は自分の知識をアップデートするために受験しました。</p>
<p>準備期間は半月ほどで、勉強に充てたのは合計 20 時間ほどです。自分の知識をアップデートするのが目的なので、ドキュメントを読むのはもちろんのこと、使ったことのないサービスは個人アカウントで有効にして試していました(会社から検証用アカウントを割り当ててもらっていますが、セキュリティ関連のサービスは AWS Organizations で管理されているので気軽に試せないのです)。</p>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">AWS のセキュリティ系のサービス、片っ端から有効にすると個人で使うにはちょっと高い💸</p>— Manabu Sakai (@manabusakai) <a href="https://twitter.com/manabusakai/status/1683041176746852353?ref_src=twsrc%5Etfw">July 23, 2023</a></blockquote>
<p>あとは、<a href="https://aws.amazon.com/jp/events/aws-event-resource/archive/">AWS サービス別資料</a>に目を通したり、公式の模擬試験を受けて不得意分野を見直しました。公式以外のリソース(試験対策の書籍や他社のトレーニング)は一切見ていません。勉強方法は 2015 年に受験したときとそれほど変わっていませんね。</p>
<ul>
<li><a href="/2015/09/aws-certification-complete/">3 か月で AWS 認定を制覇したので合格の秘訣をまとめてみた</a></li>
</ul>
<p>自分の知識をアップデートできたのはもちろんのこと、期限を決めて勉強するのは久しぶりだったので楽しく取り組めました。近いうちに、ほかのスペシャリティ認定試験も受けてみようと思います!</p>
Plausible Analytics を使って人気エントリーのランキングを生成する
2023-07-17T12:30:00+09:00
https://blog.manabusakai.com/2023/07/generate-rankings-of-popular-entries-using-plausible-analytics/generate-rankings-of-popular-entries-using-plausible-analytics
Plausible Analytics には API も用意されており、JSON 形式でデータを取り出すことができます。このデータを使って人気エントリーのランキングを出すようにしてみました。
<p>このブログのトラッキングには <a href="https://plausible.io/">Plausible Analytics</a> を使っています。オープンソースでシンプル、軽量、プライバシーに配慮し、Google Analytics の代替を謳うツールです。 Plausible Analytics には API も用意されており、JSON 形式でデータを取り出すことができます。</p>
<ul>
<li><a href="https://plausible.io/docs/stats-api">Stats API reference | Plausible docs</a></li>
</ul>
<p>このデータを使って人気エントリーのランキングを出すようにしてみました(このページの下に表示されています)。</p>
<h3>Plausible Analytics からデータを取得する</h3>
<p>過去 30 日の中で訪問者数の多い順でトップ 5 のページを取得します。 GET リクエストを送るだけの簡単なシェルスクリプトを書きます。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">set</span> <span class="nt">-eu</span>
<span class="nv">base_dir</span><span class="o">=</span><span class="si">$(</span><span class="nb">cd</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">dirname</span> <span class="nv">$0</span><span class="si">)</span><span class="s2">/.."</span><span class="p">;</span> <span class="nb">pwd</span><span class="si">)</span>
<span class="nv">env_file</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">base_dir</span><span class="k">}</span><span class="s2">/.env"</span>
<span class="k">if</span> <span class="o">[</span> <span class="nt">-f</span> <span class="s2">"</span><span class="k">${</span><span class="nv">env_file</span><span class="k">}</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
<span class="nb">.</span> <span class="s2">"</span><span class="k">${</span><span class="nv">env_file</span><span class="k">}</span><span class="s2">"</span>
<span class="k">fi
</span><span class="nv">res</span><span class="o">=</span><span class="si">$(</span>curl <span class="nt">-sS</span> <span class="nt">-H</span> <span class="s2">"Authorization: Bearer </span><span class="k">${</span><span class="nv">PLAUSIBLE_TOKEN</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
<span class="s2">"https://plausible.io/api/v1/stats/breakdown?site_id=</span><span class="k">${</span><span class="nv">PLAUSIBLE_SITE_ID</span><span class="k">}</span><span class="s2">&property=event:page&limit=5"</span><span class="si">)</span>
<span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="nv">res</span><span class="k">}</span><span class="s2">"</span> | jq <span class="nt">-r</span> <span class="s2">".results"</span>
</code></pre></div></div>
<p>このスクリプトを実行すると次のような JSON が取得できます(数字はダミー)。</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"page"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/2023/03/replace-onu-of-nuro/"</span><span class="p">,</span><span class="w">
</span><span class="nl">"visitors"</span><span class="p">:</span><span class="w"> </span><span class="mi">999</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"page"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/2023/06/config-driven-import-via-terraform-1-5/"</span><span class="p">,</span><span class="w">
</span><span class="nl">"visitors"</span><span class="p">:</span><span class="w"> </span><span class="mi">888</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"page"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/2020/03/elasticache-for-redis-with-terraform/"</span><span class="p">,</span><span class="w">
</span><span class="nl">"visitors"</span><span class="p">:</span><span class="w"> </span><span class="mi">777</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"page"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/2012/05/jquery-ajax-status-code/"</span><span class="p">,</span><span class="w">
</span><span class="nl">"visitors"</span><span class="p">:</span><span class="w"> </span><span class="mi">666</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"page"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/2021/08/review-the-kms-key-policy/"</span><span class="p">,</span><span class="w">
</span><span class="nl">"visitors"</span><span class="p">:</span><span class="w"> </span><span class="mi">555</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<h3>Jekyll の Data Files で JSON を読み込む</h3>
<p>Jekyll には <code class="language-plaintext highlighter-rouge">_data</code> ディレクトリにある YAML や JSON を自動的にロードする <a href="https://jekyllrb.com/docs/datafiles/">Data Files</a> という機能があります。先ほどの JSON ファイルを <code class="language-plaintext highlighter-rouge">_data/popular.json</code> に置くと <code class="language-plaintext highlighter-rouge">site.data.popular</code> でアクセスすることができます。</p>
<p>あとはテンプレート側で出力するだけですが、Plausible Analytics の JSON にはタイトルが含まれていないのでひと工夫が必要です。全エントリーの情報は <code class="language-plaintext highlighter-rouge">site.posts</code> に入っているので、JSON の <code class="language-plaintext highlighter-rouge">page</code> の値をもとに <code class="language-plaintext highlighter-rouge">site.posts</code> から URL が一致するオブジェクトを取り出しています。</p>
<p>具体的には次のようなテンプレートを書いています。</p>
<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code><section>
<h2>Popular Entries</h2>
<ul>
<span class="cp">{%-</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">popular_pages</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">data</span><span class="p">.</span><span class="nv">popular</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">map</span><span class="p">:</span><span class="w"> </span><span class="s1">'page'</span><span class="w"> </span><span class="cp">%}</span>
<span class="cp">{%-</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">page</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">popular_pages</span><span class="w"> </span><span class="cp">%}</span>
<span class="cp">{%-</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">post</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">posts</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">find_exp</span><span class="p">:</span><span class="w"> </span><span class="s1">'post'</span><span class="p">,</span><span class="w"> </span><span class="s1">'post.url contains page'</span><span class="w"> </span><span class="cp">%}</span>
<li>
<a href="<span class="cp">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">replace</span><span class="p">:</span><span class="w"> </span><span class="s1">'index.html'</span><span class="p">,</span><span class="w"> </span><span class="s1">''</span><span class="w"> </span><span class="cp">}}</span>"><span class="cp">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">title</span><span class="w"> </span><span class="cp">}}</span></a>
<time datetime="<span class="cp">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s1">'%Y-%m-%d %H:%M'</span><span class="w"> </span><span class="cp">}}</span>"><span class="cp">{{</span><span class="w"> </span><span class="nv">post</span><span class="p">.</span><span class="nv">date</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">date</span><span class="p">:</span><span class="w"> </span><span class="s1">'%Y-%m-%d'</span><span class="w"> </span><span class="cp">}}</span></time>
</li>
<span class="cp">{%-</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="cp">%}</span>
</ul>
</section>
</code></pre></div></div>
<h3>ブログ更新時に人気エントリーのランキングを自動更新する</h3>
<p>このブログの更新頻度は月に数回なので、人気エントリーのランキングもそんなに入れ替わりません。そのためブログを更新するときにランキングも更新すれば十分です。</p>
<p>ブログの更新は GitHub Actions でビルドして S3 にアップロードしているだけなので、ビルドの前に先ほどのスクリプトを実行して <code class="language-plaintext highlighter-rouge">_data/popular.json</code> を上書きします。</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>
<span class="na">build-and-deploy</span><span class="pi">:</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
<span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">ruby/setup-ruby@v1</span>
<span class="na">with</span><span class="pi">:</span>
<span class="na">bundler-cache</span><span class="pi">:</span> <span class="kc">true</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Get Popular Entries</span>
<span class="na">run</span><span class="pi">:</span> <span class="s">./scripts/get-popular-entries.sh | tee ./_data/popular.json</span>
<span class="na">env</span><span class="pi">:</span>
<span class="na">PLAUSIBLE_TOKEN</span><span class="pi">:</span> <span class="s">${{ secrets.PLAUSIBLE_TOKEN }}</span>
<span class="na">PLAUSIBLE_SITE_ID</span><span class="pi">:</span> <span class="s">${{ vars.PLAUSIBLE_SITE_ID }}</span>
<span class="na">continue-on-error</span><span class="pi">:</span> <span class="kc">true</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build</span>
<span class="na">run</span><span class="pi">:</span> <span class="s">bundle exec jekyll build</span>
<span class="na">env</span><span class="pi">:</span>
<span class="na">JEKYLL_ENV</span><span class="pi">:</span> <span class="s">production</span>
<span class="c1"># このあとに S3 にアップロードする step が続く</span>
</code></pre></div></div>
<p>Plausible Analytics に依存してブログの更新ができないのは避けたいので、失敗してもジョブを進めるために <code class="language-plaintext highlighter-rouge">continue-on-error: true</code> を指定しています。 API リクエストに失敗したとしても、コミットしてある JSON が使われて古いランキングが表示されるだけです。</p>
<p>もともとは「<a href="https://track3jyo.com/2023/04/hugo-ranking-widgets/">Hugo で人気記事ランキング機能を AWS のノーコードなサービスで作ったで</a>」のような仕組みを考えていましたが、既存のパイプラインの延長でシンプルに実装できました。</p>
Aurora MySQL のバイナリログのパフォーマンス影響について改めて調べてみた
2023-06-27T16:40:00+09:00
https://blog.manabusakai.com/2023/06/binlog-performance-in-aurora-mysql/binlog-performance-in-aurora-mysql
知識をアップデートするために改めて調べてみると、Aurora MySQL 2.10 以降はバイナリログのパフォーマンスへの影響は気にしなくても良さそうです。
<p>Aurora MySQL で RDS Blue/Green Deployments を使うにはバイナリログを有効にする必要があります。</p>
<p>Aurora MySQL 1 の頃はバイナリログを有効にすると、パフォーマンスへの影響があるので必要なときだけ有効にすることが推奨されていました。 re:Post の「<a href="https://repost.aws/ja/knowledge-center/aurora-mysql-increase-binlog-retention">Aurora MySQL 互換 DB クラスターのバイナリログの保持期間を長くするにはどうすればよいですか?</a>」にも次のように記載されています。</p>
<blockquote>
<p>注: Aurora MySQL 互換 DB クラスターでバイナリログを有効にすると、パフォーマンスに以下の影響が生じます。</p>
<ul>
<li>追加の書き込みオーバーヘッドが発生する(必要な場合にのみ有効にしてください)</li>
<li>バイナリログのリカバリプロセスにより、再起動時のエンジンの起動時間が長くなる</li>
</ul>
</blockquote>
<p>知識をアップデートするために改めて調べてみると、Aurora MySQL 2.10 以降はバイナリログのパフォーマンスへの影響は気にしなくても良さそうです。以下でその理由を見ていきます。</p>
<h3>binlog I/O cache</h3>
<p>1 つ目の理由は、Aurora MySQL 2.10 で導入された binlog I/O cache という機能です。 db.t2 / db.t3 を除くインスタンスクラスではデフォルトで有効になっています。</p>
<ul>
<li><a href="https://aws.amazon.com/jp/blogs/database/introducing-binlog-i-o-cache-in-amazon-aurora-mysql-to-improve-binlog-performance/">Introducing binlog I/O cache in Amazon Aurora MySQL to improve binlog performance | AWS Database Blog</a></li>
</ul>
<p>直近にコミットされたバイナリログのイベントをキャッシュすることで、binlog dump スレッド(バイナリログの内容をレプリカに送信するスレッド)のパフォーマンスを向上させています。</p>
<p>Aurora MySQL 2.09 以前は <code class="language-plaintext highlighter-rouge">aurora_binlog_replication_max_yield_seconds</code> というパラメータで調整していました。このパラメータを使うと binlog dump スレッドが指定した秒数待つようになり、バイナリログの書き込みと読み込みの競合を減らすことでパフォーマンスを改善していました。しかし、言い換えればレプリケーション遅延が大きくなることを意味します。 binlog I/O cache はこの問題も解決してくれます。</p>
<p>実際に Aurora MySQL 2.11.2 のクラスターでバイナリログを有効にしてみましたが、Datadog APM では差は見られませんでした。</p>
<h3>enhanced binlog</h3>
<p>2 つ目の理由は、最近発表されたばかりの Aurora MySQL 3.03 以降で使える enhanced binlog という機能です。この機能は binlog I/O cache と違い、明示的に有効にする必要があります。</p>
<ul>
<li><a href="https://aws.amazon.com/jp/blogs/database/introducing-amazon-aurora-mysql-enhanced-binary-log-binlog/">Introducing Amazon Aurora MySQL enhanced binary log (binlog) | AWS Database Blog</a></li>
</ul>
<p>大前提としてバイナリログはデータベースへの変更のコミット順序を維持しなければなりません。この処理に 2 フェーズコミットが採用されていますが、この調整のために書き込みの待ち時間が発生します。</p>
<p>enhanced binlog はトランザクションログとバイナリログのストレージを分離し、バイナリログは最適化された専用のストレージノードに書き込みます。そして、バイナリログのソートと順序付けをデータベースエンジンではなくストレージレイヤーで行っています。</p>
<blockquote>
<p>The first innovation comes from separating storage of the transaction log from storage of the binlog. Instead of using the same storage node for storing both the transaction and binlog, Aurora MySQL now stores the binlog on specialized storage nodes optimized for binlog. These storage nodes have added logic which make it possible for the database engine to push down the sorting and ordering of binlog to the storage layer.</p>
</blockquote>
<p>MySQL の binlog の仕組み(上記のブログ記事では community binlog と呼んでいます)を Aurora 向けに再設計したのが enhanced binlog と言えるでしょう。</p>
<p>enhanced binlog は別のストレージに保存されるためバックアップに含まれないなどの制約もあります。なので、binlog I/O cache でも満足できない場合に enhanced binlog を検討すると良さそうです。</p>
Terraform 1.5 で実現する config-driven なインポート作業
2023-06-18T08:50:00+09:00
https://blog.manabusakai.com/2023/06/config-driven-import-via-terraform-1-5/config-driven-import-via-terraform-1-5
Terraform 1.5 で追加された import ブロックと -generate-config-out フラグを組み合わせると、既存のパイプラインの中で安全にインポート作業が行えるようになります。
<p>Terraform 1.5 で追加された <code class="language-plaintext highlighter-rouge">import</code> ブロックと <code class="language-plaintext highlighter-rouge">-generate-config-out</code> フラグを組み合わせると、既存のパイプラインの中で安全にインポート作業が行えるようになります。 Hashicorp ではこのことを "config-driven import" と呼んでいます。</p>
<ul>
<li><a href="https://www.hashicorp.com/blog/terraform-1-5-brings-config-driven-import-and-checks">Terraform 1.5 brings config-driven import and checks</a></li>
</ul>
<p>これまでは <code class="language-plaintext highlighter-rouge">terraform import</code> コマンドでリソースを 1 つずつインポートし、それを定義する HCL も自分で書く必要がありました。コマンドを実行すると即座に tfstate に反映されるので、HCL がマージされるまでの間にほかの人が誤ってリソースを削除してしまう危険性がありました。そのため、チームで共同作業するリポジトリでは気軽にインポートできませんでした。</p>
<p>Terraform 1.5 はこれらの問題をスマートに解決してくれます。</p>
<h3>既存のパイプラインでインポートするステップ</h3>
<p>チームで共同作業する場合、すでに何らかのパイプラインが存在すると思います。その中で config-driven なインポート作業を行うには、次のようなステップになると思います。</p>
<ol>
<li>インポートしたいリソースを <code class="language-plaintext highlighter-rouge">import</code> ブロックに定義する</li>
<li>ローカルで <code class="language-plaintext highlighter-rouge">-generate-config-out</code> フラグを付けて <code class="language-plaintext highlighter-rouge">terraform plan</code> を実行する</li>
<li>生成された HCL をチェックして、不要な引数を削除したり、ベタ書きの値を直す</li>
<li>ローカルで <code class="language-plaintext highlighter-rouge">terraform plan</code> を実行して差分がないことを確認する</li>
<li>HCL をコミットして Pull request を作成し、チームメンバーにレビューしてもらう</li>
<li>レビューが通ったらマージして、パイプラインから <code class="language-plaintext highlighter-rouge">terraform apply</code> を実行してインポートする</li>
<li>(オプション)不要になった <code class="language-plaintext highlighter-rouge">import</code> ブロックを削除する</li>
</ol>
<p>自分の理解を深めるために、Terraform Cloud で実際にインポートしてみました。</p>
<h3>インポートの検証に使うリソースを作成する</h3>
<p>今回は AWS CLI で IAM user を作成して、それを上記のステップでインポートしてみます。</p>
<p>まず IAM policy を作成します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat test-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:Get*",
"s3:List*"
],
"Resource": [
"arn:aws:s3:::my-bucket/*"
]
}
]
}
$ aws iam create-policy --policy-name test-policy --policy-document file://test-policy.json
</code></pre></div></div>
<p>次に IAM user を作って先ほどの policy をアタッチします。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws iam create-user --user-name test-user
$ aws iam attach-user-policy --policy-arn arn:aws:iam::xxxxxxxxxxxx:policy/test-policy --user-name test-user
</code></pre></div></div>
<h3>-generate-config-out フラグを付けて HCL を生成する</h3>
<p>既存のリポジトリの中に <code class="language-plaintext highlighter-rouge">import.tf</code> を用意します。 <code class="language-plaintext highlighter-rouge">id</code> で指定する値は <code class="language-plaintext highlighter-rouge">terraform import</code> で指定するものと同じです。コマンドでインポートするのと違い、複数のリソースをまとめてインポートできるのは楽ですね。</p>
<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">import</span> <span class="p">{</span>
<span class="nx">id</span> <span class="p">=</span> <span class="s2">"test-user"</span>
<span class="nx">to</span> <span class="p">=</span> <span class="nx">aws_iam_user</span><span class="p">.</span><span class="nx">test_user</span>
<span class="p">}</span>
<span class="nx">import</span> <span class="p">{</span>
<span class="nx">id</span> <span class="p">=</span> <span class="s2">"arn:aws:iam::xxxxxxxxxxxx:policy/test-policy"</span>
<span class="nx">to</span> <span class="p">=</span> <span class="nx">aws_iam_policy</span><span class="p">.</span><span class="nx">test_policy</span>
<span class="p">}</span>
<span class="nx">import</span> <span class="p">{</span>
<span class="nx">id</span> <span class="p">=</span> <span class="s2">"test-user/arn:aws:iam::xxxxxxxxxxxx:policy/test-policy"</span>
<span class="nx">to</span> <span class="p">=</span> <span class="nx">aws_iam_user_policy_attachment</span><span class="p">.</span><span class="nx">test_policy</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">-generate-config-out</code> フラグを付けて <code class="language-plaintext highlighter-rouge">terraform plan</code> を実行します。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ terraform version
Terraform v1.5.0
on linux_arm64
$ terraform init
$ terraform plan -generate-config-out=test_user.tf
(snip)
Plan: 3 to import, 0 to add, 0 to change, 0 to destroy.
╷
│ Warning: Config generation is experimental
│
│ Generating configuration during import is currently experimental, and the generated configuration format may change in future versions.
╵
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Terraform has generated configuration and written it to test_user.tf. Please review the configuration and edit it as necessary before adding it to version control.
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">test_user.tf</code> を確認すると次のような HCL が生成されていました。</p>
<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># __generated__ by Terraform</span>
<span class="c1"># Please review these resources and move them into your main configuration files.</span>
<span class="c1"># __generated__ by Terraform from "test-user/arn:aws:iam::xxxxxxxxxxxx:policy/test-policy"</span>
<span class="k">resource</span> <span class="s2">"aws_iam_user_policy_attachment"</span> <span class="s2">"test_policy"</span> <span class="p">{</span>
<span class="nx">policy_arn</span> <span class="p">=</span> <span class="s2">"arn:aws:iam::xxxxxxxxxxxx:policy/test-policy"</span>
<span class="nx">user</span> <span class="p">=</span> <span class="s2">"test-user"</span>
<span class="p">}</span>
<span class="c1"># __generated__ by Terraform from "test-user"</span>
<span class="k">resource</span> <span class="s2">"aws_iam_user"</span> <span class="s2">"test_user"</span> <span class="p">{</span>
<span class="nx">force_destroy</span> <span class="p">=</span> <span class="kc">null</span>
<span class="nx">name</span> <span class="p">=</span> <span class="s2">"test-user"</span>
<span class="nx">path</span> <span class="p">=</span> <span class="s2">"/"</span>
<span class="nx">permissions_boundary</span> <span class="p">=</span> <span class="kc">null</span>
<span class="nx">tags</span> <span class="p">=</span> <span class="p">{}</span>
<span class="nx">tags_all</span> <span class="p">=</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="c1"># __generated__ by Terraform from "arn:aws:iam::xxxxxxxxxxxx:policy/test-policy"</span>
<span class="k">resource</span> <span class="s2">"aws_iam_policy"</span> <span class="s2">"test_policy"</span> <span class="p">{</span>
<span class="nx">description</span> <span class="p">=</span> <span class="kc">null</span>
<span class="nx">name</span> <span class="p">=</span> <span class="s2">"test-policy"</span>
<span class="nx">name_prefix</span> <span class="p">=</span> <span class="kc">null</span>
<span class="nx">path</span> <span class="p">=</span> <span class="s2">"/"</span>
<span class="nx">policy</span> <span class="p">=</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">Statement</span><span class="se">\"</span><span class="s2">:[{</span><span class="se">\"</span><span class="s2">Action</span><span class="se">\"</span><span class="s2">:[</span><span class="se">\"</span><span class="s2">s3:Get*</span><span class="se">\"</span><span class="s2">,</span><span class="se">\"</span><span class="s2">s3:List*</span><span class="se">\"</span><span class="s2">],</span><span class="se">\"</span><span class="s2">Effect</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">Allow</span><span class="se">\"</span><span class="s2">,</span><span class="se">\"</span><span class="s2">Resource</span><span class="se">\"</span><span class="s2">:[</span><span class="se">\"</span><span class="s2">arn:aws:s3:::my-bucket/*</span><span class="se">\"</span><span class="s2">]}],</span><span class="se">\"</span><span class="s2">Version</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">2012-10-17</span><span class="se">\"</span><span class="s2">}"</span>
<span class="nx">tags</span> <span class="p">=</span> <span class="p">{}</span>
<span class="nx">tags_all</span> <span class="p">=</span> <span class="p">{}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>このままでも間違いはありませんが、不要な引数やベタ書きされた値、ワンライナーで書かれた policy などを直します。</p>
<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_iam_user"</span> <span class="s2">"test_user"</span> <span class="p">{</span>
<span class="nx">name</span> <span class="p">=</span> <span class="s2">"test-user"</span>
<span class="nx">path</span> <span class="p">=</span> <span class="s2">"/"</span>
<span class="p">}</span>
<span class="k">data</span> <span class="s2">"aws_iam_policy_document"</span> <span class="s2">"test_policy"</span> <span class="p">{</span>
<span class="nx">statement</span> <span class="p">{</span>
<span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
<span class="s2">"s3:Get*"</span><span class="p">,</span>
<span class="s2">"s3:List*"</span><span class="p">,</span>
<span class="p">]</span>
<span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"arn:aws:s3:::my-bucket/*"</span><span class="p">]</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">resource</span> <span class="s2">"aws_iam_policy"</span> <span class="s2">"test_policy"</span> <span class="p">{</span>
<span class="nx">name</span> <span class="p">=</span> <span class="s2">"test-policy"</span>
<span class="nx">path</span> <span class="p">=</span> <span class="s2">"/"</span>
<span class="nx">policy</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_iam_policy_document</span><span class="p">.</span><span class="nx">test_policy</span><span class="p">.</span><span class="nx">json</span>
<span class="p">}</span>
<span class="k">resource</span> <span class="s2">"aws_iam_user_policy_attachment"</span> <span class="s2">"test_policy"</span> <span class="p">{</span>
<span class="nx">policy_arn</span> <span class="p">=</span> <span class="nx">aws_iam_policy</span><span class="p">.</span><span class="nx">test_policy</span><span class="p">.</span><span class="nx">arn</span>
<span class="nx">user</span> <span class="p">=</span> <span class="nx">aws_iam_user</span><span class="p">.</span><span class="nx">test_user</span><span class="p">.</span><span class="nx">name</span>
<span class="p">}</span>
</code></pre></div></div>
<p>再度 <code class="language-plaintext highlighter-rouge">terraform plan</code> を実行して差分がないことを確認します。</p>
<h3>パイプラインからインポートを実行する</h3>
<p>HCL をコミットし Pull request を作成します。 Terraform Cloud の場合はインポート対象のリソースが分かりやすく表示されます。</p>
<p><img src="/image/upload/2023/plan-for-import.png" alt="インポートの plan 結果" /></p>
<p>あとは通常のレビュープロセスを経てマージし、パイプラインから <code class="language-plaintext highlighter-rouge">terraform apply</code> を実行すればインポート作業は完了です。 <code class="language-plaintext highlighter-rouge">import</code> ブロックはもう不要なので削除できます。</p>
<h3>おわりに</h3>
<p>リソースのインポートはさまざまな OSS が存在していますが、個人的にはどれも決定打に欠けるものでした。そんな中 Terraform 1.5 が示したソリューションは、ユーザーのペインをよく理解しているなと感じました。</p>
<p>これで手動で作られた AWS リソースを効率よくインポートしていけそうです!</p>
Terraform Cloud から S3 に tfstate を移行する
2023-05-31T11:30:00+09:00
https://blog.manabusakai.com/2023/05/migrate-tfstate-from-terraform-cloud-to-s3/migrate-tfstate-from-terraform-cloud-to-s3
Terraform Cloud から S3 に tfstate を移行しようとすると、まだ実装されていないというエラーが表示されます。とはいえ、tfstate さえコピーすれば移行できると思うので試してみました。
<p>Terraform Cloud への移行手順は公式ドキュメントの "<a href="https://developer.hashicorp.com/terraform/tutorials/cloud/cloud-migrate">Migrate State to Terraform Cloud</a>" でも丁寧に解説されていますが、Terraform Cloud から別の backend に移行する手順は公式には用意されていません。</p>
<p>たとえば、Terraform Cloud から S3 に tfstate を移行しようとすると、まだ実装されていないというエラーが表示されます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ terraform init
Initializing the backend...
Migrating from Terraform Cloud to backend "s3".
╷
│ Error: Migrating state from Terraform Cloud to another backend is not yet implemented.
│
│ Please use the API to do this: https://www.terraform.io/docs/cloud/api/state-versions.html
│
│
╵
</code></pre></div></div>
<p>とはいえ、tfstate さえコピーすれば移行できると思うので試してみました。</p>
<p>今回検証で使ったのは Terraform v1.4.6 (linux_arm64) です。</p>
<h3>tfstate の移行手順</h3>
<p>まず、Terraform Cloud にある tfstate を pull します。念のためバックアップも取っておきます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ terraform state pull > terraform.tfstate
$ cp terraform.tfstate terraform.tfstate.backup
</code></pre></div></div>
<p>次に <code class="language-plaintext highlighter-rouge">override.tf</code> を作成して backend を <code class="language-plaintext highlighter-rouge">local</code> に変更します。 <code class="language-plaintext highlighter-rouge">override.tf</code> に <code class="language-plaintext highlighter-rouge">terraform</code> ブロックがあると、もとの設定の <code class="language-plaintext highlighter-rouge">cloud</code> もしくは <code class="language-plaintext highlighter-rouge">backend</code> を常に上書きしてくれます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat << EOF > override.tf
terraform {
backend "local" {
path = "terraform.tfstate"
}
}
EOF
</code></pre></div></div>
<p>backend を変更したので init し直します。 <code class="language-plaintext highlighter-rouge">-reconfigure</code> を付けることで pull してきた tfstate を変更せずにそのまま使います。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ terraform init -reconfigure
Initializing the backend...
Successfully configured the backend "local"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing provider plugins...
(snip)
Terraform has been successfully initialized!
</code></pre></div></div>
<p>続いて backend を <code class="language-plaintext highlighter-rouge">local</code> から <code class="language-plaintext highlighter-rouge">s3</code> に変更します。先ほど作成した <code class="language-plaintext highlighter-rouge">override.tf</code> をさらに書き換えます。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat << EOF > override.tf
terraform {
backend "s3" {
bucket = "mybucket"
key = "path/to/my/key"
region = "ap-northeast-1"
}
}
EOF
</code></pre></div></div>
<p>backend を変更したので再度 init し直します。次は <code class="language-plaintext highlighter-rouge">-migrate-state</code> を付けて tfstate を S3 にコピーします。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ terraform init -migrate-state
Initializing the backend...
Terraform detected that the backend type changed from "local" to "s3".
Do you want to copy existing state to the new backend?
Pre-existing state was found while migrating the previous "local" backend to the
newly configured "s3" backend. No existing state was found in the newly
configured "s3" backend. Do you want to copy this state to the new "s3"
backend? Enter "yes" to copy and "no" to start with an empty state.
Enter a value: yes
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing provider plugins...
(snip)
Terraform has been successfully initialized!
</code></pre></div></div>
<p>ここで plan を実行して "No changes." になることを確認します。</p>
<p>問題なければ <code class="language-plaintext highlighter-rouge">override.tf</code> に書いた一時的な backend と <code class="language-plaintext highlighter-rouge">provider.tf</code> などにあるもとの設定をマージします。あとは最初に pull してきた <code class="language-plaintext highlighter-rouge">terraform.tfstate</code> を削除して完了です。</p>
Mac のメールアプリでスマートメールボックスが消える
2023-05-12T09:30:00+09:00
https://blog.manabusakai.com/2023/05/smart-mailbox-disappears/smart-mailbox-disappears
自分の環境ではメールアプリを再起動すると、なぜか作成したはずのスマートメールボックスが消えてしまいます。
<p>独自ドメインで運用しているプライベートのメールアドレスは iCloud のカスタムメールドメインを使っています。もともとは iCloud から Gmail に転送していたのですが、Gmail のプライバシーに不安を感じてからは転送するのをやめました。</p>
<p>それ以来、iCloud のメールは標準のメールアプリで読んでいます。 Mac, iPhone, iPad でユーザー体験が統一されているのは、純正アプリならではのメリットですね。</p>
<h3>アプリを再起動するとスマートメールボックスが消える</h3>
<p>Mac のメールアプリでは、指定した条件に基づいてメッセージを自動的に 1 つのメールボックスにまとめることができる<a href="https://support.apple.com/ja-jp/guide/mail/mlhlp1190/mac">スマートメールボックス</a>が使えます。</p>
<p>ですが、自分の環境(macOS Ventura 13.3.1、メールアプリ 16.0)ではメールアプリを再起動すると、なぜか作成したはずのスマートメールボックスが消えてしまいます。より正確にはデフォルトで設定されている「今日」というスマートメールボックスだけになります。サポートコミュニティで検索すると、同じような現象を訴えている方がちらほら見つかります。</p>
<p>スマートメールボックスの設定は以下の plist ファイルに保存されています(メールアプリのバージョンによって <code class="language-plaintext highlighter-rouge">V10</code> の部分は変わってくるようです)。</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">~/Library/Mail/V10/MailData/SmartMailboxesLocalProperties.plist</code></li>
</ul>
<p>スマートメールボックスを作ると確かにこのファイルに追記されていましたが、アプリを起動するタイミングでデフォルトの内容に戻っていました。</p>
<p>また、上記のファイル名で検索して分かったのですが、本来は同じディレクトリに <code class="language-plaintext highlighter-rouge">SyncedSmartMailboxes.plist</code> というファイルがあるはずなのに見当たりません。おそらく、このファイルが存在しないせいで起動のたびにデフォルトに戻ってしまっているようです。</p>
<h3>原因は iCloud Drive で同期していること?</h3>
<p>ここで問題を切り分けるために、まだメールアプリを使ったことがない別のマシンで、別の Apple ID のアカウントをセットアップしてみました。するとスマートメールボックスが消えることはなく、また <code class="language-plaintext highlighter-rouge">SyncedSmartMailboxes.plist</code> も存在していました。</p>
<p>このことから、iCloud で同期していることが怪しそうという当たりがつきました(iCloud が登場したころにいろいろトラブルを経験したので、何となくそんな気はしていました)。</p>
<p>そこでシステム設定の「iCloud Drive に同期しているアプリケーション」からメールの項目をオフにしてみました。</p>
<p><img src="/image/upload/2023/icloud-drive-sync.png" alt="iCloud Drive に同期しているアプリケーション" /></p>
<p>すると起動したタイミングで <code class="language-plaintext highlighter-rouge">SyncedSmartMailboxes.plist</code> が作成され、スマートメールボックスが消えることもなくなりました! この plist ファイルが存在する状態になれば、先ほどのメールの項目をオンに戻しても大丈夫でした。</p>
<p>iCloud Drive で同期しているデータに何らかの問題があったのだと思いますが、iCloud Drive の同期をオフにしたことで正しい状態に戻り、さらに再びオンにしたことでそのデータが同期されたのでしょう。</p>