マネジメントアカウントで取得したドメインの静的ウェブサイトをメンバーアカウントから CloudFormation を使用して Cloudfront + S3 + Certificate Manager で構築する

静的ウェブサイトが必要になった経緯

個人開発している Web サービスで決済機能として Stripe を導入予定です。同社の審査は大きく分けて「本人証明」と「ビジネス証明」の 2 つがあり、後者で特商法のページが必要になります。この特商法のページを静的なウェブサイトとして構築するのが本記事の主旨となります。

設計

基本的には S3 の静的ウェブサイト機能 を使用します。ただし、S3 の同機能は HTTPS に対応していないので前段に Cloudfront + Certificate Manager を設置する必要があります。使用する AWS はサービスは以下になります。

サービス 用途 リソースを所有するアカウント リソースを作成するアカウント AWS Organizations の OU
Route53 ドメイン取得, DNS 管理 マネジメントアカウント メンバーアカウント OU: Infrastructure/Pord/website-prd
S3 オブジェクトストレージ マネジメントアカウント メンバーアカウント OU: Infrastructure/Pord/website-prd
Cloudfront CDN マネジメントアカウント メンバーアカウント OU: Infrastructure/Pord/website-prd
Certificate Manager SSL 証明書管理 マネジメントアカウント メンバーアカウント OU: Infrastructure/Pord/website-prd

AWS Organizations のベストプラクティス に則り OU: Infrastructure/Prod/website-prd という メンバーアカウント で構築します。

本来であれば上記のリソースはマネジメントアカウントではなくメンバーアカウントに所有させたかったのですが、構築に AWS から提供されている CloudFormation のテンプレートである amazon-cloudfront-secure-static-site を使用しているため *1 、このような設計としています(ベストプラクティス上はマネジメントアカウントは 同アカウントで必要な最小限のリソースのみを所有するのみにとどめ、ほとんどのリソースはメンバーアカウントが所有する ことが推奨されていますが、これは数少ない例外かもしれません)

今回はメンバーアカウントからマネジメントアカウントの強めの権限を設定した Role に Switch Role して CloudFormation を実行しています。具体的には以下のような設定になります。

マネジメントアカウント > IAM

  1. マネジメントアカウントの IAM で Role: ManagementAccountAdministrator を作成する。同 Role には AWS Managed Policy: AdministratorAccess を付与する。Trusted relationships としてメンバーアカウントを Principal に追加する。
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::<account_id_of_member_account>:root"
            },
            "Action": "sts:AssumeRole",
            "Condition": {}
        }
    ]
}

マネジメントアカウント > IAM Identity Center

  1. メンバーアカウントの IAM Identity Center の Permission Set: Administrator *2 に Inline Policy でマネジメントアカウントで作成した Role: ManagementAccountAdministrator への STS 権限を付与する。
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PrivilegeEscalationToManagementAccountAdministrator",
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::<account_id_of_management_account>:role/ManagementAccountAdministrator"
    }
  ]
}

以下は IAM Identity Center で管理している User: kyagi がメンバーアカウント: B の Adminitrator グループからマネジメントアカウント: A の Role: ManagementAccountAdministrator にスイッチした例です。

スイッチ前のマネージメントコンソロールのヘッダです。

スイッチする時のダイアログです。

スイッチ後のマネージメントコンソロールのヘッダです。

実装

S3 を使用した静的ウェブサイト構築については AWS にドキュメントが用意されています。S3 発のドキュメントCloudfront 発のドキュメント があります。設定をひとつひとつ確認しながらマニュアルで進めるなら前者ですが、ここでは Cloudformation のテンプレートを紹介している後者を使用しています。

このテンプレートではドメインを取得したアカウントの Route53 から取得した HOSTED ZONE ID を入力として求められます。またリージョンは us-west-1 を想定した実装になっています。私の場合、Route53 のドメイン取得はマネジメントアカウントで管理するようにしているので、ここでは上記で言及したようにメンバーアカウントからマネジメントアカウントに switch role して CloudFormation を実行しています。

所感

一口に、特商法のサイトを構築すると言っても、なかなか手間がかかることがわかりました。今回は AWS から提供されている CloudFormation のテンプレートを使用したのでだいぶ楽でしたが手動で設定する場合はめんどくさそうです。

特商法のサイトに載せるべき主な情報をクラウドサービスに対応させると以下になります。所在地の記載は DMM バーチャルオフィスを契約予定です。

ID 項目 説明 クラウドサービス
1 所在地 事業所の住所(登記簿上の住所)*個人事業主の場合は「請求があったら遅滞なく開示します」で省略可。 〒123-4567 東京都渋谷区○○町○○丁目12-3 DMM バーチャルオフィス
2 電話番号 顧客からの日本語の問い合わせに対応できる電話番号*個人事業主の場合は「請求があったら遅滞なく開示します」で省略可。 03-1234-5678 受付時間 10:00-18:00(土日祝を除く) DMM バーチャルオフィス、もしくは、専用のスマフォ + SIM
3 メールアドレス 顧客からの問い合わせに対応できるメールアドレス contact@example..jp AWS Workmail

参考情報

「特定商取引法に基づく表記」ページの作成方法

静的ウェブサイト構築

*1:同テンプレートが要求する Route53 の hosted zone id がドメインを取得したアカウントを想定しているため

*2:個人的に IAM Identity Center における Group, Permission Set, Predefined Policy の設定は こちら

TypeScript w/ Zod でリテラルな文字列の配列を型として扱う

Zod の Schema Validation でリテラルな文字列の数パターンのみを受け付けるようにする

z.enum() にリテラルな文字列の配列を渡すことでその配列のみを有効な入力になるように制限することができます。

(悪いね、今日はカツオ(bonito) は入っていないんだ)

import { z } from 'zod';

const menuAvailableToday = ['tuna', 'mackerel', 'eel', 'squid', 'flounder', 'yellowtail', 'amberjack'];
const orderAcceptable = z.object({
    order: z.enum(menuAvailableToday)
});

let yourOrder;

// This passes.
yourOrder = orderAcceptable.parse({ order: 'tuna' });
console.log(`You ordered ${yourOrder.order}`);

// This fails.
// yourOrder = orderAcceptable.parse({ order: 'bonito' });
// console.log(`You ordered ${yourOrder.order}`);

固定化された数パターンの画像ファイル名のみを受け付けるようにする

ここでは、期待される特定のファイル名(サイズ + 拡張子)のパターンを生成しています。 TypeScript の標準ライブラリには順列組み合わせの関数が用意されていないようなので、array-combination/src/combination.ts at main · idw111/array-combination · GitHub を使用しています。

  • 1040.png, 700.png, 460.png, 300.png, 200.png
  • 1040.jpg, 700.jpg, 460.jpg, 300.jpg, 200.jpg
  • 1040.jpeg, 700.jpeg, 460.jpeg, 300.jpeg, 200.jpeg

z.parse() ではなく z.safeParse() だと型チェックに失敗した時のエラーに応じて処理が可能です。

handle_array_of_literal_string_as_type.ts

import { z } from 'zod';

// https://github.com/idw111/array-combination/blob/main/src/combination.ts
const combination = <T extends any>(...arrays: T[][]): T[][] => {
    if (arrays.length === 1) return arrays[0].map((item) => [item]);
    const [firstArray, ...restArrays] = arrays;
    return combination(...restArrays)
        .map((result) => firstArray.map((item) => [item, ...result]))
        .flat();
};

const permutations: string[][] = combination(['1040', '700', '460', '300', '200'], ['png', 'jpg', 'jpeg']);

// FIXME: `candidates: string[]` fails with `the error TS2769: No overload matches this call.`
const candidates: any = permutations.map(element => element[0].concat('.', element[1]));
const imageFile = z.object({
    fileName: z.enum(candidates)
});

const imf1 = imageFile.parse({ fileName: '1040.png' });
console.log(imf1)

// const imf2 = imageFile.parse({ fileName: '1041.png' });
// console.log(imf2)

ファイル名が所定の組み合わせ以外の場合は ZodError で弾かれます。

➜ node_modules/.bin/tsc handle_array_of_literal_string_as_type.ts --target es2016 --module nodenext --moduleResolution nodenext
➜ node --import tsx handle_array_of_literal_string_as_type.ts
{ fileName: '1040.png' }

/Users/kyagi/lab/skeleton-zod-001/node_modules/zod/lib/types.js:43
                const error = new ZodError_1.ZodError(ctx.common.issues);
                              ^

ZodError: [
  {
    "received": "1041.png",
    "code": "invalid_enum_value",
    "options": [
      "1040.png",
      "700.png",
      "460.png",
      "300.png",
      "200.png",
      "1040.jpg",
      "700.jpg",
      "460.jpg",
      "300.jpg",
      "200.jpg",
      "1040.jpeg",
      "700.jpeg",
      "460.jpeg",
      "300.jpeg",
      "200.jpeg"
    ],
    "path": [
      "fileName"
    ],
    "message": "Invalid enum value. Expected '1040.png' | '700.png' | '460.png' | '300.png' | '200.png' | '1040.jpg' | '700.jpg' | '460.jpg' | '300.jpg' | '200.jpg' | '1040.jpeg' | '700.jpeg' | '460.jpeg' | '300.jpeg' | '200.jpeg', received '1041.png'"
  }
]
(... snip ...)

Jest でのテスト例

safeParse() の返り値とデータの両方をチェックするテストの例です。

handle_array_of_literal_string_as_type.test.js

import { imageFile } from "./handle_array_of_literal_string_as_type";

describe('1040.png is a valid file name', () => {
    const result = imageFile.safeParse({
        fileName: "1040.png"
    });
    test('fileName: 1040.png is OK', () => {
        expect(result.success).toBe(true);
    });
    test('fileName: 1040.png is OK', () => {
        expect(result.data.fileName).toBe("1040.png");
    });
});

describe('1041.png is NOT a valid file name', () => {
    const result = imageFile.safeParse({
        fileName: "1041.png"
    });
    test('fileName: 1041.png is NG', () => {
        expect(result.success).toBe(false);
    });
});
➜ node_modules/.bin/jest handle_array_of_literal_string_as_type.test.js
 PASS  ./handle_array_of_literal_string_as_type.test.js
  1040.png is a valid file name
    ✓ fileName: 1040.png is OK (2 ms)
    ✓ fileName: 1040.png is OK (4 ms)
  1041.png is NOT a valid file name
    ✓ fileName: 1041.png is NG (1 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.698 s, estimated 1 s
Ran all test suites matching /handle_array_of_literal_string_as_type.test.js/i.

参考情報

zod.dev

github.com

個人アカウントでも AWS Organizations と IAM Identity Center を使う a.k.a. 古の AWS アカウントにかけられた東京リージョン az-a の呪いからの脱出

アカウント温故知新と東京リージョン az-a の呪い

昔話になりますが、私は個人の AWS アカウントを 東京リージョンが利用可能になった 2011年 に作成しました。それ以降 EC2 をはじめとして散発的に AWS リソースを使用してきましたが、時代とともに古いアカウントならではの制限事項が目につくようになってきました。

制限事項の中で最も大きいのが 古い AWS アカウントでは東京リージョンのアベイラビリティゾーン a が使用できない という問題です。AWS CDK しかり、Cfn しかり、何かのハンズオンしかり、基本的に新しめの(?) AWS アカウントを想定しており、その度に「他の人のアカウントではうまくいくが自分のアカウントだとエラーになってしまう」ケースが「稀ではなくよくある」事態となっていました。

こういった経緯を含め、アカウント管理の体制を一新して AWS Organizations + IAM Identity Center での運用に切り替えたところ AWS のユーザー体験はかなり向上しました(古いアカウントは管理用のマネジメントアカウントとして残していますが場合によっては削除するかもしれません。また、個人アカウントとは別に同様の管理方法で個人事業向けのアカウントも別口に用意しています。こちらは将来の法人化を見据えたものになります)

AWS 公式で複数アカウントでの運用が推奨されている

複数アカウントでの運用については参考情報にあげたリンクを含めて AWS が豊富なベストプラクティスとガイドラインを出しています。主に AWS Organizations と IAM Identity Center を利用することになります。監査用に Control Tower や AWS Confing を利用してもよいですが個人アカウントでは不要なケースが多いと思います。

ただ、AWS 初心者にとってこれらのベストプラクティスとガイドラインを最初から理解し実装するのはなかなかハードルが高いのではないでしょうか。どちらかというと既に AWS でいくらかの経験があり、リソースや Billing 情報の管理で効率化を図りたい既存ユーザーが複数アカウント運用に切り替えると、ユーザー体験がかなり向上するのではないかと睨んでいます。

押さえておきたいベストプラクティス

AWS で推奨されているベストプラクティスです。AWS ハンズオンを受講した方は一番最初のスライドで「他リソースへの影響を避けるために専用のアカウントを作成することを推奨〜」という断り書きを目にした方も多いと思います。アカウントは「目的ごとにフットワーク軽く作る」ことがプラットフォーム側からも期待されているのです。

  • アカウントにおける AWS Organizations の OU 構造とメールアドレス構成要素を一致させる。こうすることでどのアカウントにどのメールアドレスが紐づいているのか一目瞭然になり管理がしやすくなる(以下は、AWS のドキュメントで紹介されているベストプラクティスの具象化を私なりに行なった結果です。例として John Smith さん(jsmith@gmail.com) さんが xenogeneic という会社で admantine と bomburst という 2 つのサービスを AWS で開発かつデプロイしている場合、アカウント運用は以下のようになります)
AWS アカウント アカウントのメールアドレス AWS Organizations の Organization Unit および配下のメンバーアカウント
xenogeneic jsmith+aws+xenogeneic@gmail.com Management Account
adamantine-prod jsmith+aws+adamantine+workload+prod@gmail.com Workload/Prod/adamantine-prod
bomburst-prod jsmith+aws+bomburst+workload+prod@gmail.com Workload/Prod/bomburst-prod
bomburst-dev jsmith+aws+bomburst+workload+sdlc+dev@gmail.com Workload/SDLC/bomburst-dev
handson-20240103 jsmith+aws+xenogeneic+sandbox+handson-20240103@gmail.com Sandbox/handson-20240103
xenogeneic-infrastucture jsmith+aws+xenogeneic+infrastructure+prod@gmail.com Infrastructure/Prod
xenogeneic-security jsmith+aws+xenogeneic+security+prod@gmail.com Security/Prod
  • AWS の新機能の試用やハンズオンは一時利用するアカウントを OU: Sandbox 配下に作成して実施する。こうすることでアカウント単位で Billing 情報を独立させ可視化/監視することができ、結果的にリソースの無駄遣いを防ぐことができる。基本的に Sandbox 配下のアカウントで料金が発生することはない。ハンズオン実施時にアカウントを削除してしまってもよい。
  • AWS Organizations のルートの頂点となる Management アカウントは管理専用にし、開発やテストなどを行わない。つまり、ワークロードを持たせない。

※OU: Organization Unit, SDLC: Software Development Life Cycle

スクリーンショット

IAM Identity Center のポータル経由のログイン例

Sandbox 配下のアカウント運用例

個人的なベストプラクティス

以下は私が個人的に採用している運用上の工夫です。

  • IAM Identity Center における Group と Permission Set を一致させています。Rails におけるテーブル名(複数)とモデル名(単数) の Naming Convention を適用しています。BizPeople には営業やマーケティング担当を含みます(基本的に非エンジニアに与えるポリシーは AWS で見るべき情報は Predefined permissions で用意されている Billing のみ になる ので営業、マーケティング、経理を別々にする必要はないいう判断)
Group Permission Set Predefined Policies
Administrators Administrator AdministratorAccess
SREs SRE SystemAdministrator
Engineers Engineer ReadOnlyAccess (デフォルトはこれで、ここから ABAC で Switch Role させる)
BizPeople BizPerson Billing
  • IAM Identity Center や ABAC の運用/確認のためにダミーの架空ユーザーをあらかじめ作成しておく。アクセスコントロールの確認はこれらのダミーユーザーのメンバーアカウントを通して行う。
Username Name Primary Email Title Division Department
ramuro Ray Amuro [DefaultEmailAccountBeforeAtMark]+aws+[ProjectName]+ramuro@gmail.com sfe gundam origin
myashima Mirai Yashima [DefaultEmailAccountBeforeAtMark]+aws+[ProjectName]+myashima@gmail.com sre gundam origin
blinks Banagher Links [DefaultEmailAccountBeforeAtMark]+aws+[ProjectName]+blinks@gmail.com sfe gundam unicorn
mlzabi Mineva Lao Zabi [DefaultEmailAccountBeforeAtMark]+aws+[ProjectName]+mlzabi@gmail.com salesrep gundam unicorn

※sfe: Software Engineer, sre: site reliability engineer, salesrep: sales representative

IAM Identity Center を使う場合は aws configure sso で期限付きの credentials 情報を作成する

aws configure sso を利用して期限付きの credentials 情報を発行したのちに aws cli や SDK を利用します。

~/.aws/cli/cache 配下に期限付きの AWS ACCESS KEY と SECRET ACCESS KEY が発行されキャッシュされた .json が作成されます。またこちらと対になる形で ~/.aws/sso/cache 配下にはセッション情報がキャッシュされた .json が作成されます。結果的に通常の ~/.aws/credentials は不要になります。

➜ aws configure sso

参考情報

Rails でカレンダーチャート(いわゆる Github の草) を描画する

Rubygem: rails_charts から Apache ECharts を使用する

核となるのは JavaScript のライブラリ Apache ECharts です。このライブラリを利用して Canvas API ベースの 2D グラフィックを描画することができます。各言語でのラッパーライブラリが存在し、Rails では rails_charts を利用することで view から簡単に様々なチャートを作成することができます。

カレンダーチャートとはヒートマップである

Apache ECharts では様々なチャートを描画できますが、カレンダーチャートは Heatmap がベースとなります。Heatmap を拡張したのが Calendar です。描画時に使用可能なオプションもこちらの 2 つから流用できます。

インストールと設定

rails_charts の README に従います。Rails7 の場合、編集するファイルは以下の 2 つです。

  • config/importmap.rb
  • app/javascript/application.js

README 中の Commit.for_calendar_chart の実体は test で使用している ダミーアプリのモデル から確認できます。

適当なカレンダーのデータを与えて描画させた例がこちらです。

➜ cat app/views/home/index.html.erb
<%= calendar_chart(
  {
    data: [ [Date.new(2023,1,1), 1],
    [Date.new(2023,2,2), 5],
    [Date.new(2023,3,3), 10],
    [Date.new(2023,4,4), 40]
    ]
  },
  class: 'box',
  options: {
    visualMap: {
      show: true,
      min: 0,
      max: 40,
      orient: 'horizontal',
      inRange: {
        color: ['#DAFADA', '#008000']
      }
    },
    calendar: [{
      range: '2023',
    },]
  })
%>

カレンダーチャートの使い方

使用しているモデルを created_at と count で「日付 + 数値」に変換し rails_charts で描画することで、日々の活動記録を可視化できます。Github のコミット履歴の活用例は言うに及ばず、継続の度合いを示すインジケーターとして利用の幅は広そうです。日々の勉強や運動、ダイエット記録などにも適用できると思います。

irb(main):013:0> Flashcard.group_by_day(:created_at).count.to_a.last(10)
  Flashcard Count (0.8ms)  SELECT COUNT(*) AS "count_all", DATE_TRUNC('day', "flashcards"."created_at"::timestamptz AT TIME ZONE 'Etc/UTC')::date AS "date_trunc_day_flashcards_created_at_timestamptz_at_time_zone_e" FROM "flashcards" WHERE ("flashcards"."created_at" IS NOT NULL) GROUP BY DATE_TRUNC('day', "flashcards"."created_at"::timestamptz AT TIME ZONE 'Etc/UTC')::date
=> [[Wed, 06 Dec 2023, 3], [Thu, 07 Dec 2023, 0], [Fri, 08 Dec 2023, 1], [Sat, 09 Dec 2023, 1], [Sun, 10 Dec 2023, 0], [Mon, 11 Dec 2023, 3], [Tue, 12 Dec 2023, 1], [Wed, 13 Dec 2023, 4], [Thu, 14 Dec 2023, 2], [Fri, 15 Dec 2023, 4]]

Google Charts or Apache ECharts

Google Charts の Calendar Chart と似ていますが関連までは調べていません。

参考情報

github.com github.com echarts.apache.org

Notion の Engineering Wiki テンプレートを使ってチームの情報共有を効率化する

Engineering Wiki のテンプレートが優秀

Notion が標準で提供しているテンプレートを物色したところ、Engineering Wiki がとても良かったので紹介します。

タグによるドキュメントの集約ができるので目的のドキュメントに到達しやすい

ドキュメントをタグごとに group by で集約でき、その結果をタブで切り替えられるので目的のドキュメントを簡単に見つけることができます。また、スペースの利用効率が向上するので、欲しい情報を探してスクロールする必要がなくなります。集約結果の表示方法(ビュー)はリストビューやギャラリビューにすることができるので、ビュー内のソート方法を更新日時にしておくと更新された記事を見分けやすくなります。

トップ画面(左ブロックがタグ一覧、右ブロックがドキュメント一覧)

ドキュメントの編集画面

ビュー画面のギャラリー表示

ビュー画面のソートオプション

ドキュメントの Verification プロパティで賞味期限切れを防ぐ

ドキュメントにはあらかじめ Verification プロパティが用意されているので、これを利用することでレビュープロセスのワークフローを構築するのがよさそうです。Verification の有効期限を 1 週間後、1 ヶ月後、3 ヶ月後と設定しておくことで定期的なアップデートを促す通知としても活用できます。

wiki 配下の範囲検索ボタンが組み込まれていて検索しやすい

API を使用する

API を使用するには Integration を作成した後、右上のハンバーガーメニューから Add Connections を選択して配下のページへのアクセスを許可する必要があります。

試しに Query a database を使ってみましたが、かなり詳細なオブジェクトが返ってきました。Engineering Wiki 自体がひとつのデータベースとなっており識別子として 8a2409b31ac94fcc8269b743e1fb681d が振られています。情報の抽出やバックアップ、自動更新などいろいろな用途がありそうです。

➜ curl -X POST 'https://api.notion.com/v1/databases/8a2409b31ac94fcc8269b743e1fb681d/query' -H 'Authorization: Bearer '"secret_<THIS_IS_SECRET>"'' -H 'Notion-Version: 2022-06-28' -H "Content-Type: application/json" --data '{
  "filter": {
    "or": [
      {
        "property": "Tags",
        "multi_select": {
          "contains": "Codebase"
        }
      }
   ]
  },
  "sorts": [
    {
      "property": "Page",
      "direction": "ascending"
    }
  ]
}' | jq -r '.results[]|select(.object == "page")|.properties.Page.title[].text.content, .url'

Backend
https://www.notion.so/Backend-98ec69babcf545179a8e1bc92bccbbf9
Code Reviews
https://www.notion.so/Code-Reviews-2dc09abb21974f4ba2616fc93517987c
How to QA
https://www.notion.so/How-to-QA-5dd1f96216c84a7cab53c1f50c037c6c
React
https://www.notion.so/React-f9b7185c06a245219768c317ebb804ea
Useful Commands
https://www.notion.so/Useful-Commands-371c609aea5440abb7456d9dfc6e0d8f

developers.notion.com

AWS EventBridge でメトリクスとイベントの両ターゲットを対象にした通知フローを構築する

メトリクスに応じた処理とイベントに応じた処理を SNS to Chatbot の通知フローのレールに集約する

ここ数ヶ月 AWS EventBridge を利用した通知フローの改善を通して AWS Observability について、その全体像の理解が深まりました。このエントリでは AWS EventBridge を利用した通知フローについて最近の Input Transformer(w/ Input Path, Input Template) の機能追加も含めてその概要を説明します(抽象的な内容が多くなるので実際に EventBridge の使用経験がないと理解しにくいと思いますがご容赦ください)

結論から言うと CloudWatch Alarm の「生の(加工されない)」通知と、それをイベントとして捕捉して加工した「カスタム」通知の 2 通りをそれぞれの通知チャンネルで管理するのがよいのではないかという設計に辿り着きました。ただ、これはあくまで個人的なベストプラクティスです。EventBridge は AWS の中でも比較的新しいサービスであり 2023年3月に他の AWS サービスからイベントを EventBridge に送信することが可能になり、2023年9月に Input Transformer(w/ Input Path, Input Template) で Chatbot に送る通知内容のカスタマイズが可能になるなど進化を続けているので、現状では AWS 側もベストプラクティスが提供できていない雰囲気を感じています。

上記の図だと 3 段階のインシデントレベル(= sev: severity level) ごとに Slack のチャンネルを用意して以下のように運用するのがよいと考えています(Slack だとチャンネル名は alphabetical にソートされるので最もインシデントレベルが高い 1 が上にくるのもよいと思います)

  • projectX-alerts-raw[123]: メトリクスに応じた CloudWatch Alarm の「生の(加工されない)」通知。人間が読むことを期待していない。
  • projectX-alerts-sev[123]: イベントに応じた CloudWatch Alarm State Change を EventBridge の InputTransformer(w/ Input Path, Input Template) で「カスタム」した通知。人間が読むことを期待している。こちらの通知に「インシデントレベル」「担当チームのメンション」「次にやること」を載せる。

CloudWatch Alarm にはもともと In Alarm/OK 時に通知する SNS topic を指定することができました。ただ、通知内容のフォーマットを変更することはできず、一部のマークダウンに必要な情報を記載するのみが可能です(日本語もこの一部のマークダウンのみ)。

EventBridge も通知内容のフォーマットを変更することはできなかったのですが、2023/09/12 にリリースされた Input Transformer((w/ Input Path, Input Template)で通知内容を変更することができるようになりました(日本語も可能)。この点、通知内容の柔軟性は EventBridge のほうが上かもしれません。 Custom notifications are now available for AWS Chatbot

(余談ですが、ここでの AWS Chatbot は SNS に対する Subscriber であると同時に Slack に対する Publisher になります。Chatbot は EventBridge > SNS と流れてきたメッセージを Slack の chat.postMessage の API 仕様に基づいて HTTP POST してくれるということです)

メトリクスベース(メトリクスに応じたターゲットによる処理) の通知フロー

CloudWatch Alarm を使用する方法で一般的な通知フローです。

metrics from AWS Service > CloudWatch Alarm > SNS > Chatbot > Slack

イベントベース(イベントに応じたターゲットによる処理) の通知フロー

EventBridge を使用する方法で補足するイベントパターンを source や detail-type といった属性で絞り込むことができます。Input Template(w/ Input Path, Input Template) を指定して変数に格納した値を通知に載せることも可能です。

events from AWS Service > EventBridge > SNS > Chatbot > Slack events as CloudWatch Alarm State Change > EventBrdige > SNS > Chatbot > Slack

EventBridge による様々なイベント捕捉パターン

AWS のサービスが発するイベントは Event Structure と呼ばれる共通の json フォーマットの仕様に基づいています。EventBridge で補足するイベントは Source に対象の AWS サービスを指定した後に detail でそれぞれのサービスの持つ固有のパラメータで絞り込むことができます。

例として特定の ElastiCache for Redis のクラスタのイベントを補足するイベントパターンは以下になります。ここで readOnly はイベントが read 系(List, Get, Describe) かそうでないかを表しています。フェイルオーバーなどのイベントは readOnly: false になるのでリソースに何かしらの状態遷移が発生した場合を補足する際に役立ちます。readOnly に似た属性に managementEvent があります。ただもともとデフォルトで read 系のイベントは EventBridge には送られない仕様 となっています。これは AWS Management Console や CLI で状態を確認するための処理、つまり状態遷移が発生しない状態を、わざわざイベントとして処理する必要がないからだと思われます。

{
  "source": ["aws.elasticache"],
  "detail": {
    "eventSource": ["elasticache.amazonaws.com"],
    "requestParameters": {
      "replicationGroupId": ["my-elasticache-dev"]
  },
  "readOnly": [false]
}

これらのイベントは CloudWatch LogGroup をイベントして補足するように設定した後、CloudWatch LogGroup のログを見るのが手っ取り早く確認できます。EventBridge の Input Transformer(w/ Input Path, Input Template) でも CloudWatch LogGroup のログをコピー&ペーストできるようになっているので便利です。なので EventBridge の Rule を作成する場合はまずは Target に SNS Topic ではなく CloudWatch LogGroup を指定して対象の AWS サービスのイベントログの json を吐き出させて、後々の Rule で Input Transformer のフォームに貼り付けるのが有用です。

ひとつ面白いイベントパターンとしては CloudWatch Alarm State Change をイベントして補足できることです(ややこしいのですが「アラームの状態が変化したというイベント」になります)。これを利用すると CloudWatch Alarm が Alarm/OK/Insufficient data といった状態遷移が発生した時にメトリクス経由の通知とイベント経由の通知の 2 種類で通知を飛ばすことが可能になります。2 種類通知が飛ぶのは冗長なので冒頭に挙げた人間が読む用のチャンネル(イベントベース)と生の通知用のチャンネル(メトリクスベース) の 2 つにわけるとよいと思います。

{
  "source": ["aws.cloudwatch"],
  "detail-type": ["CloudWatch Alarm State Change"]
}

また CloudTrail の監視もイベントとして処理できるので監査プロセスにも使用できます。

{
   "source" : [ "aws.sts" ],
   "detail-type": ["AWS API Call via CloudTrail"],
   "detail" : {
     "eventName" : ["AssumeRole"]
   }
}

EventBridge の歴史的経緯と CloudWatch からの独立

EventBridge はもともと CloudWatch Events という名称で CloudWatch の機能の一部として提供されていました。今では機能の拡充や他サービスとの繋ぎ込みによって独立したサービスとなり名称も EventBridge となっています。ただ AWS Management Console の CloudWatch 画面からは EventBridge のリンクが残っておりその名残を感じさせます(TV の歴史番組みたいになってきたな)。

時系列 メトリクスレポジトリの名称および説明 イベントレポジトリ名称および説明
CloudWatch: a metrics repository CloudWatch Events: eventbuses
CloudWatch: a metrics repository EventBridge: eventbuses and pipes(receive events and delivers to zero or more targets)

メトリクスとイベントの管理体系を分けた理由はなんでしょうか。個人的には常に発生するメトリクスといつ発生するかわからないイベントの性質の違いからではないかと推察しています。

参考情報

https://pages.awscloud.com/rs/112-TZM-766/images/AWS-21_Observe_and_detect_for_application_availability_and_performance_KMD05-KMD13.pdf docs.aws.amazon.com Custom notifications are now available for AWS Chatbotaws.amazon.com docs.aws.amazon.com

docker buildx でマルチプラットフォームのイメージを作成する その2

MacOS Ventura で buildx を利用して amd64 と arm64 のイメージを作成する

docker buildx でマルチプラットフォームのイメージを作成する - blog.ayakumo.net で紹介した buildx を利用して amd64 と arm64 のイメージを作成していきます。環境は以下になります。

  • ビルドマシン(docker コマンド実行環境): Mac mini(2018) Intel Core i7
  • OS: Mac OS Ventura 13.3.1
  • docker: Docker version 20.10.24, build 297e128

Docker ドキュメンテーションの Building multi-platform images を参考に 3 つのステップごとに実行していきます。

You can build multi-platform images using three different strategies that are supported by Buildx and Dockerfiles:

  1. Using the QEMU emulation support in the kernel
  2. Building on multiple native nodes using the same builder instance
  3. Using a stage in Dockerfile to cross-compile to different architectures

https://docs.docker.com/build/building/multi-platform/

1. QMENU エミュレーションを使用するために tonistiigi/binfmt をインストールする

➜ docker run --privileged --rm tonistiigi/binfmt --install all

2. マルチプラットフォーム用のビルダーインスタンスを作成する

ここでは Docker のドキュメンテーションに倣ってビルダーインスタンスの名前を mybuilder としていますが intel-and-arm-builder など出力先のプラットフォームがわかる名前にしてもよいかもしれません。

➜ docker buildx create --name mybuilder --driver docker-container --bootstrap
[+] Building 3.0s (1/1) FINISHED
 => [internal] booting buildkit                                                                                                                    3.0s
 => => pulling image moby/buildkit:buildx-stable-1                                                                                                 2.2s
 => => creating container buildx_buildkit_mybuilder0                                                                                               0.8s
mybuilder

➜ docker buildx use mybuilder

➜ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT             STATUS  BUILDKIT PLATFORMS
mybuilder *     docker-container
  mybuilder0    unix:///var/run/docker.sock running v0.11.6  linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
default         docker
  default       default                     running 20.10.24 linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux   docker
  desktop-linux desktop-linux               running 20.10.24 linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

➜ docker buildx inspect
Name:          mybuilder
Driver:        docker-container
Last Activity: 2023-05-13 06:25:09 +0000 UTC

Nodes:
Name:      mybuilder0
Endpoint:  unix:///var/run/docker.sock
Status:    running
Buildkit:  v0.11.6
Platforms: linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6

3. Dockerfile を編集して指定したプラットフォーム上のコンテナイメージを作成する

該当コミットはこちらになります: https://github.com/kyagi/ec5/commit/b5bf4488d130e539743eca265b027b43fd5eb9bb

➜ vi Dockerfile
➜ docker buildx build --platform linux/amd64,linux/arm64 .

Dockerfile で echo しているメッセージが対象プラットフォームごとに出力されるのでわかりやすいですね。

このままだとコンテナイメージはキャッシュされるだけでコンテナレジストリには登録されないため --push でレジストリに送ります。

➜ docker buildx build --platform linux/amd64,linux/arm64 -t kyagi/ec5:latest --push .

コンテナレジストリ上で amd64, arm64 の両方のイメージが格納されていることを確認できました。

参考情報

asciinema と agg: ターミナルセッションの記録と共有

百聞は一見に一見にしかず?

AWS EC2 のコスト計算 API を Rust で作成した - blog.ayakumo.net で紹介したツールの README を作成するにあたり asciinema と agg*1 を使用しています。ターミナルセッションを GIF にすることで、ツールの動作や実行環境が読み手にわかりやすくなります。もともとは https://github.com/ducaale/xh の README で使用されていたのを見たのがきっかけで、今回、自分のツールでも採用してみました。:-)

基本的な使いかた

  1. asciinema rec で録画用のターミナルセッションを開始すると新たに bash が起動されるので録画したいコマンドを打ちこんでいきます。
➜ asciinema rec
(... snip ...)
  1. exit で録画を終了します。そのまま asciinema.org に .cast ファイルをアップロードするかローカルに保存するかを選択します。保存した .cast ファイルは asciinema play で再生します。ローカルで保存した .cast を後から asciinema upload でアップロードすることも可能です。
➜ exit
asciinema: recording finished
asciinema: press <enter> to upload to asciinema.org, <ctrl-c> to save locally
asciinema: asciicast saved to /var/folders/yr/f0mycbg912b6hjt6tbrkh9j80000gn/T/tmpj433lze8-ascii.cast

➜ asciinema play /var/folders/yr/f0mycbg912b6hjt6tbrkh9j80000gn/T/tmpj433lze8-ascii.cast
➜ asciinema upload /var/folders/yr/f0mycbg912b6hjt6tbrkh9j80000gn/T/tmpj433lze8-ascii.cast
  1. agg を使用して .cast を .gif に変更します。同時に再生速度やターミナルのテーマ、フォントサイズなどを変更します。github の README で使用する場合は読み手のストレスを考えた速度にするとよさそうです。
➜ agg --theme monokai --font-size 20 --speed 2 /var/folders/yr/f0mycbg912b6hjt6tbrkh9j80000gn/T/tmpj433lze8-ascii.cast ~/Desktop/ec5-demo.gif

起動と終了は古のscript コマンドと同じです。打ち込むコマンドをタイピングしている最中にミスしてしまうとちょっと恥ずかしいのでコピペにするか、もしくはゆっくりでもいいので確実に入力していきます。.gif にする場合は agg を使用して再生速度を変更可能なので、確実に入力していったほうが最終的な見栄えはよくなると思います。

ソフトウェア開発の現場でも

ターミナル操作を動画にしておくのはソフトウェア開発のいろいろな場面で有用だと思うので活用していきたいと考えています。プロジェクトで使う場合はプライベートに .cast を保存して共有するのがよいかもしれません。ただ、基本的に 一時停止ができないので複雑な操作をする場合は QuickTime などを利用して .mov のほうがいいでしょうか...

参考情報

*1:agg はもともと asciicast2gif という名前で開発されていたようです

AWS EC2 のコスト計算 API を Rust で作成した

AWS EC2 のコスト計算をターミナルで行う

AWS EC2 のコスト計算を行う API を Rust で作成しました。フレームワークは Actix Web を使用しています。

github.com

以下の特徴とターミナルで JSON を加工できる点で、AWS 公式の オンデマンドインスタンス料金リザーブドインスタンス料金 または vantage といったサービスと比べて使いやすいかもしれません。

  • 結果がプログラマブルであること(レスポンスは JSON)
  • 単価だけでなく個数を指定して計算が可能であること
  • オンデマンドとリザーブド各種の料金を比較できること(各種: Upfront と Term の組み合わせ)
  • ドル/円の両方が表示できること

Rust 製 API をどこで使うべきか

Web サービスのバックエンドとして内部的に使用するマイクロサービスなどを作っていくのがよさそうです。

参考情報

Amazon Linux 2023 用の user_data.txt を作成する

AL2023 with Ruby, Python, Node.js, and Rust

EC2 環境を AL2023(Amazon Linux 2023) に切り替えるべく準備を進めています。AL2025 がリリースされる 2025年まで の 2 年間のベースとする予定です。

まずは作って壊せるスタンドアロンの開発環境(workstation)からはじめます。私の場合 1. VPC, Subnet, RouteTable, InternetGateway ... etc のレイヤは CloudFormation が担当し、2. EC2 や RDS のレイヤを ansible が担当します。個人的な開発環境の動作要件は以下になります。

  • mandatory:
    • Ruby, Python, Node.js, Rust の最新バージョンが利用可能であること
    • 必要なツール群が利用可能であること(jq, rg, fd, fzf)
  • optional:
    • ec2 instance connect で接続できること

僕が作った最強の user_data

こちらの user_data.txt を t4g.small で走らせると、インスタンスの起動から構築完了までの所要時間は約 20 分でした。

➜ pyenv exec ansible-playbook -i hosts site.yml --extra-vars "@workstation.json"

gist.github.com

➜ cat roles/spawn/tasks/main.yml
---
- name: Spawn an ec2 instance
  amazon.aws.ec2_instance:
    name: "{{ instance_name }}"
    key_name: "{{ key_name }}"
    vpc_subnet_id: "{{ vpc_subnet_id }}"
    instance_type: "{{ instance_type }}"
    security_groups: "{{ security_groups }}"
    network:
      assign_public_ip: true
      private_ip_address: "{{ private_ip_address }}"
    image_id: "{{ image_id }}"
    user_data: "{{ lookup('file', 'user_data.txt') }}"
    volumes:
      - device_name: /dev/xvda
        ebs:
          volume_size: 30
          volume_type: gp2
          delete_on_termination: true
    tags:
      Owner: "{{ owner }}"
      Stack: "{{ stack }}"
      Project: "{{ project }}"
      Application: "{{ application }}"
      Ec2InstanceConnectAccess: "{{ ec2_instance_connect_access }}"
➜ cat workstation/workstation.json
{
  "instance_name": "workstation",
  "key_name": "__key-name__",
  "vpc_subnet_id": "__subnet-id__",
  "instance_type": "t4g.small",
  "security_groups": ["__sg-id1__", "__sg-id2__"],
  "private_ip_address": "10.0.0.10",
  "image_id": "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64",
  "owner": "kyagi",
  "stack": "deveplopment",
  "project": "workstation",
  "application": "hodgepodge of development tools",
  "ec2_instance_connect_access": "true"
}

シンボリックリンク的に指定できるイメージ ID 設定

上記の workstation.json では resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64 のようにイメージ ID を指定しています。AL2023 から(?) image id がシンボリックリンク的に指定できるようになっています。リンク先の image id は自動的に最新のものが参照されるので ansible や Cfn で更新をする必要がなくなりました。

The following are dynamic kernel specifications. The default kernel version automatically changes with each major kernel version update.

al2023-ami-kernel-default-arm64 for arm64 architecture
al2023-ami-minimal-kernel-default-arm64 for arm64 architecture (minimal AMI)
al2023-ami-kernel-default-x86_64 for x86_64 architecture
al2023-ami-minimal-kernel-default-x86_64 for x86_64 architecture (minimal AMI)

https://docs.aws.amazon.com/linux/al2023/ug/get-started.html

EC2 instance connect 用のタグ設定

タグ Ec2InstanceConnectAccess: true が指定された EC2 は AWS Management Console から EC2 Instance Connect ができるように設定します。別途 Ec2 Instance Connect にはリージョンごとの特定の IP 範囲からの接続をセキュリティグループで許可する必要がある で紹介した IAM 設定が必要です。

参考情報