コネヒト開発者ブログ

コネヒト開発者ブログ

SOCIインデックスによるECSのデプロイ時間短縮について検証しました

こんにちは。2023年7月に入社しました、開発部プラットフォームグループ インフラエンジニアの @yosshi です。今回はSOCIインデックスによるECSのデプロイ時間の短縮効果について検証を行ったので、その検証内容と結果を共有したいと思います。

デプロイ時間短縮の方法は様々あると思いますが、SOCIインデックスはイメージやデプロイフローに手を入れずに、比較的簡単に取り入れられそうだったので導入を検討しました。

SOCI(Seekable OCI)インデックスとは

SOCIインデックスとは、イメージの遅延読み込み(非同期読み込み)を可能にする技術です。イメージ全体をダウンロードする前にコンテナイメージから個々のファイルを抽出できるようにし、コンテナを高速に起動することができます。

既存のコンテナイメージにあるファイルのインデックス (SOCI インデックス) を作成することによって機能します。

利用条件

  • コンテナイメージがx86_64もしくはARM64アーキテクチャであること
  • Linuxプラットフォームバージョンが1.4.0であること

参考記事

SOCIインデックスの適用方法

SOCIインデックスを作成する方法は、公式で用意されているAWS SOCI Index Builderを使用する方法と、手動で作成する方法の2パターンがありますが、今回はより簡単に導入できそうなAWS SOCI Index Builderを使用する方法を選択しました。

AWS SOCI Index Builderは、 AWS クラウド内のコンテナイメージのインデックスを作成するためのサーバーレスソリューションで、公式よりCloudFormaitonテンプレートが用意されています。

今回はこのCloudFormationテンプレートを利用し、Terraformで適用していきます。
以下のようなイメージです。

resource "aws_cloudformation_stack" "soci_index_builder" {
  name         = "soci-index-builder"
  template_url =  "https://aws-quickstart.s3.us-east-1.amazonaws.com/cfn-ecr-aws-soci-index-builder/templates/SociIndexBuilder.yml" <span style="color: #d32f2f">#公式で用意されているSOCIのテンプレートのパス</span>
  capabilities = ["CAPABILITY_IAM"]

  parameters = {
    "SociRepositoryImageTagFilters" = "<リポジトリ名>:<タグ名>" #SOCIインデックスの適用範囲のフィルターをかける
  }
}

AWS SOCI Index Builderのアーキテクチャ

EventBridge・Lambda・IAM・CloudWatchなどのリソースが作成されます。
引用: https://aws-ia.github.io/cfn-ecr-aws-soci-index-builder/

SOCIインデックス作成の流れ

  1. ECRイメージアクションイベントを検出しフィルタリング用のAWS Lambdaを呼び出す。
  2. CloudFormationのパラメータで提供されたフィルタに一致するイメージアクションイベントをフィルタリング。
  3. 一致するイメージのSOCIインデックスを生成し、ECRレジストリのイメージリポジトリにインデックスをプッシュバックする。

作成される各リソースの詳細を知りたい方は引用元のリンク先をご確認ください。

検証方法

SOCIインデックスの検証にあたり、今回2段階で実施しました。

  1. SOCIインデックスの効果検証

    • 実際のDev(開発)環境に適用する前に、まずはSOCIインデックスに本当に効果がありそうか調べました。
    • 現行のDev環境と同一のECSクラスター内にSOCIインデックス適用済みの別サービスを立て現行と起動時間の比較をしています。
  2. 現行の開発環境での動作検証
    • 検証1でSOCIインデックスの一定の効果が見られたので、実際に使用している開発環境にSOCIインデックスを適用し比較しました。

それでは検証内容について詳しく説明していきます。

前提条件

弊社では以下環境を使用しています 。

  • ECS on Fargate
  • AWSリソースの反映:Terraform
  • ECSのデプロイ:ecspresso

検証1:SOCIインデックスの効果検証

まずはDev環境の同一クラスター内にSOCIインデックス適用済み別サービスを立てて現行との起動時間を比較しました。

リソースの作成はTerraformで行っています。まずは必要なリソースを作成していきます。

ECRリポジトリ

resource "aws_ecr_repository" "example_soci_test" {
  name = "example-soci-test"

  image_scanning_configuration {
    scan_on_push = true
  }
}

SOCI Index BuilderのCloudFormationテンプレート
example-soci-testリポジトリの全てにSOCIインデックスを適用するようフィルターを設定しています。

resource "aws_cloudformation_stack" "soci_index_builder" {
  name         = "soci-index-builder"
  template_url =  "https://aws-quickstart.s3.us-east-1.amazonaws.com/cfn-ecr-aws-soci-index-builder/templates/SociIndexBuilder.yml"
  capabilities = ["CAPABILITY_IAM"]

  parameters = {
    "SociRepositoryImageTagFilters" = "example-soci-test:*"
  }
}

Terraformを使用し作成したリソースを適用します。

$ terraform apply

次に作成したECRリポジトリにイメージをプッシュします。

Dockerfileは従来のDev環境と同じものを使用しています。

イメージのビルド

$ docker build . -t example-soci-test:latest

リポジトリにイメージをプッシュできるように、イメージにタグ付け

$ docker tag example-soci-test:latest xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/example-soci-test:latest

ECRにイメージをプッシュ

$ docker push xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/example-soci-test:latest

AWSコンソール上でECRの対象リポジトリの画面から、作成したイメージに対して、アーティファクトタイプがSoci Index, Image Indexのイメージが別途作成されていることがわかります。この作成されたSOCIインデックス適用済みのイメージを使用します。

次にこのSOCIインデックス適用済みのイメージを使用しECS環境へデプロイします。

弊社ではデプロイツールとしてecspressoを使用しているため、ecspressoを利用しローカルからデプロイしたいと思います。(ecspressoを使用していない場合は別の方法でのデプロイを実施してください)

ecspresso initで既存のDev環境の設定ファイルをローカルに持ってきます。

$ ecspresso init \
--config config.yaml \
--region ap-northeast-1 \
--cluster <Dev環境のクラスター名> \
--service <Dev環境のサービス名>

実行することでローカルに3つのファイルが作成されます。

  • config.yaml
  • ecs-service-def.json
  • ecs-task-def.json

作成されたファイルの内容を今回テストする内容に書き換えます。

  • config.yaml
region: ap-northeast-1
cluster: example-cluster #Dev環境と同じクラスターを使用
service: example-soci-test-service #SOCIインデックス適用するためのサービス
service_definition: ecs-service-def.json
task_definition: ecs-task-def.json
timeout: "10m0s"
  • ecs-service-def.json
{
  "capacityProviderStrategy": [
    {
      "base": 1,
      "capacityProvider": "FARGATE_SPOT",
      "weight": 1
    }
  ],
  "deploymentConfiguration": {
    "deploymentCircuitBreaker": {
      "enable": false,
      "rollback": false
    },
    "maximumPercent": 200,
    "minimumHealthyPercent": 100
  },
  "deploymentController": {
    "type": "ECS"
  },
  "desiredCount": 1,
  "enableECSManagedTags": false,
  "enableExecuteCommand": false,
  "healthCheckGracePeriodSeconds": 0,
  "launchType": "",
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "assignPublicIp": "ENABLED",
      "securityGroups": [
        <security-group> #現行で使用しているセキュリティグループを利用
      ],
      "subnets": [
        <subnet> #現行で使用しているsubnetを利用
      ]
    }
  },
  "pendingCount": 0,
  "platformFamily": "Linux",
  "platformVersion": "LATEST",
  "propagateTags": "NONE",
  "runningCount": 0,
  "schedulingStrategy": "REPLICA"
}
  • ecs-task-def.json
{
  "containerDefinitions": [
    {
      "cpu": 0,
      ],
      "essential": true,
      "image": "xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/example-soci-test:latest",
      "name": "example-soci-test",
    }
  ],
  "cpu": "256",
  "executionRoleArn": <execution-role-arn>, #現行で使用しているexecution roleを利用
  "family": "example-soci-test-task",
  "ipcMode": "",
  "memory": "512",
  "networkMode": "awsvpc",
  "pidMode": "",
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "taskRoleArn": <task-role-arn> #現行で使用しているtask roleを利用
}

ファイルの作成が完了したら、ローカルからecspressoで適用していきます

$ ecspresso deploy --config <config.yamlのパス>

反映されたことが確認できたら、環境の準備が整っているので起動のためのテストしていきます。

以下のシェルスクリプトを用意し、現在のDev環境とSOCIインデックス適用済みの環境でそれぞれ実行することで、どの程度起動時間に差があるのか確認します。

CLUSTER=example-cluster #現行のDev環境のクラスター
TASKDEF={テスト対象のタスク定義}
REGION=ap-northeast-1
NUM_OF_TASKS=1
TASKS=$(aws ecs list-tasks \
    --cluster $CLUSTER \
    --family $TASKDEF \
    --region $REGION \
    --query "taskArns[:${NUM_OF_TASKS}]" \
    --output text)

aws ecs describe-tasks \
    --tasks $TASKS \
    --region $REGION \
    --cluster $CLUSTER \
    --query "tasks[] | reverse(sort_by(@, &createdAt)) | [].[{startedAt: startedAt, createdAt: createdAt, taskArn: taskArn}]" \
    --output table

実行結果

SOCIインデックス適用前(現行のDev環境):1分05秒

---------------------------------------------------------------------------------------------------------------------
|                                                   DescribeTasks                                                   |
+-----------+-------------------------------------------------------------------------------------------------------+
| createdAt |  2023-08-14T12:39:13.360000+09:00                                                                     |
| startedAt |  2023-08-14T12:40:18.763000+09:00                                                                     |
|  taskArn  |  arn:aws:ecs:ap-northeast-1:xxxxxxxxxx:task/example-cluster/xxxxxxxxxxxxxxxxxxxxxxxx                  |
+-----------+-------------------------------------------------------------------------------------------------------+

SOCIインデックス適用済みの環境:36秒(29秒の短縮)

---------------------------------------------------------------------------------------------------------------------
|                                                   DescribeTasks                                                   |
+-----------+-------------------------------------------------------------------------------------------------------+
| createdAt |  2023-08-14T19:42:05.469000+09:00                                                                     |
| startedAt |  2023-08-14T19:42:41.185000+09:00                                                                     |
|  taskArn  |  arn:aws:ecs:ap-northeast-1:xxxxxxxxxx:task/example-cluster/xxxxxxxxxxxxxxxxxxxxxxx                   | 
+-----------+-------------------------------------------------------------------------------------------------------+

30秒弱起動時間が短縮されたことがわかります。

検証2:現行のDev環境にSOCIインデックスを適用

上記のテストである程度効果がありそうなことがわかったので、次に実際にDev環境に対してSOCIインデックスを適用していきます。

弊社ではブランチ環境をDev環境にデプロイする際、branch_deployのイメージタグを使用しています。 よって今回はbranch_deployのイメージタグを使用している全ての対象に対してSOCIインデックスを適用していきます。

再度Indexbuilderのフィルターに今回の対象を追加していきます。

resource "aws_cloudformation_stack" "soci_index_builder" {
  name         = "soci-index-builder"
  template_url =  "https://aws-quickstart.s3.us-east-1.amazonaws.com/cfn-ecr-aws-soci-index-builder/templates/SociIndexBuilder.yml"
  capabilities = ["CAPABILITY_IAM"]

  parameters = {
    "SociRepositoryImageTagFilters" = "example-soci-test:*, *:branch_deploy" 
  }
}

Dev環境での検証は、Github Actionsのデプロイ時間をもとに計測していきます。

  • Github Actionsの内容
    ※ 弊社で適用している内容から必要な項目のみ抜粋して記載しています。
name: Manually deploy to development

on:
  workflow_dispatch

env:
  IMAGE_TAG: ${{ (github.ref == 'refs/heads/main' && github.sha) || 'branch_deploy' }}
  AWS_ROLE_ARN: <aws_role_arn>

permissions:
  id-token: write
  contents: read
  actions: read
  issues: write
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      ref_link: ${{ env.REF_LINK }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      - name: Build, tag, and push image to Amazon ECR
        if: github.ref != 'refs/heads/main'
        env:
          DOCKER_BUILDKIT: 1
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: <リポジトリ名>
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG --build-arg GITHUB_ACCESS_TOKEN=${{ secrets.MACHINE_USER_GITHUB_ACCESS_TOKEN }} .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
      - name: Build, tag, and push image to Amazon ECR for main
        if: github.ref == 'refs/heads/main'
         # branch_depoloyタグ以外の処理が書いてあるので省略
         # ...

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1
      - uses: kayac/ecspresso@v2
        with:
          version: v2.0.3
      - name: Deploy to Amazon ECS
        run: |
          ecspresso deploy --config <configファイルのパス>

検証結果

弊社のとあるアプリケーションを例に、SOCIインデックス適用前と後でそれぞれ直近8回のデプロイ時間を計測し比較しました。

SOCIインデックス適用前のデプロイ時間

1 2 3 4 5 6 7 8
3分44秒 3分14秒 3分50秒 2分40秒 3分53秒 4分14秒 3分47秒 4分18秒

SOCIインデックス適用後のデプロイ時間

1 2 3 4 5 6 7 8
3分12秒 2分37秒 3分13秒 3分02秒 3分27秒 2分55秒 2分58秒 2分35秒

※ Github Actions内の”deploy”部分の時間を記載しています。

8回分のデプロイ時間を平均すると以下のようになりました。

  • SOCIインデックス適用前:3分42秒
  • SOCIインデックス適用後:2分59秒
  • 短縮した時間:43秒

時間のばらつきはあるものの、平均すると上記の通りデプロイ時間を短縮できたことになります。

実際のDev環境でも一定の短縮効果があることがわかりました。

補足

Lambdaのランタイムについて

  • 公式のCloudFormationテンプレートにより作成されるLambdaのランタイムがGo.1.xを使用しており、このサポートが2023年12月31日に終了するので、この点に関しては注意が必要そうです。
  • 弊社でも将来的に新しいランタイムへの移行を計画しています。

おわりに

  • 今回SOCIインデックスを試してみて、割と簡単に導入できた上に、一定の効果があったので、簡易的なデプロイ時間短縮の手段としてはアリだと思いました。
  • Dev環境での動作が問題なさそうなことがわかったので、順次本番環境への適用も検討していきたいと思います。
  • また一方で、なかなか大幅な改善とはならなかったので、改めて他の手段(Dockerイメージの改善等)も併せて検討していく必要があると感じました。
  • プラットフォームグループでは引き続きデプロイフローの改善をはじめ、様々な開発環境の改善に向け取り組んでいきたいと思います。

第3回 リーン開発の現場輪読会 リーン開発のテクニックや戦略についてワイワイ編

こんにちは!コネヒト歴ちょうど2年になったWebエンジニアの古市(@takfjp)です。最近は岩盤浴にハマっています。サウナは熱すぎて苦手なのですが、岩盤浴だとほどよい高温でじんわり汗をかきながら全身を温められてリラックスできるので、これで季節の変わり目を乗り切れそうです。

第3回輪読会

今回は、社内有志で実施している「リーン開発の現場 カンバンによる大規模プロジェクトの運営」の輪読会の様子をお届けします!第3回となる今回は書籍の第2部である「テクニックを詳しく見る」にフォーカスし、やってみたいことや内容についての感想をワイワイ語り合いました。 これまでの様子は以下の記事をご覧ください。

第1回 リーン開発の現場輪読会 技術課題についてワイワイ編 - コネヒト開発者ブログ

第2回 リーン開発の現場輪読会 プロセス改善や WIP についてワイワイ編 - コネヒト開発者ブログ

第1部では主に、著者が携わったプロジェクトにおいてリーン開発の手法をどう実践したか・それによってどんな良い効果や学びをもたらしたかについて書かれていますが、第2部ではこれまで書籍内で触れられたリーン開発に役立つ手法・テクニックについてメインに扱われています。 今回も参加者が事前に第2部17章〜21章を読み、Miroの付箋を使って「思ったこと」「あるある」「やってみたい」という3つの分類で気づきや感想についてワイワイ話し合いました。また、自分が気になった付箋にスタンプやコメントでリアクションするようにもしました。

輪読会でワイワイと話したこと

今回も上記のボードから特に話したいことをピックアップしたり、各自が気になった付箋について深掘りしつつ語り合いました。 第2部は主に使用する各種テクニックや戦略についてフォーカスして書かれているため、それらについてどう取り組むか?という議論が進んでいきました。

ペアプロやテスト駆動開発は学習コストが高そう。それによって得られる効果を増やすには?

  • テスト駆動開発は新機能ではなく、改修などで部分的に小さく始めていく。
  • ペアプロはハードル低いけどTDDは習慣にしていかないと身につかないかも。
  • 当たり前品質に高くコミットする人に誰かがなるか、外部から呼んでくる。
    • TDDに強い人から「どうやって強くなっていったのか」なども聞きたい。

テスト自動化戦略について

  • そもそもテストしづらい構成になっているパターンがあり、つらい。
    • 長大なControllerなど、動いているがテストを書きづらいコードをうまく分割していくところに辛さがありそう。
    • 機能やリポジトリ単位でテストを書きづらいコードが集中している。そこにみんなで力を合わせてテストを集中的に追加していくことで、その後の工程がスムーズになりそう。

プランニングポーカーの粒度について

  • メンバー間で見積もる際、ポイントの粒度で±1pt(ストーリーポイント)の認識を全員で合わせるのは難しく時間がかかる。
  • S、M、L程度の粒度であれば直感的に見積もることができそう。
  • 技術者間で時間単位で見積もると、1時間程度の認識のずれが生まれることもあるが、S, M, Lの粒度で十分そう。
  • 粒度に関わらず、プランニングポーカーを通して、タスクについての認識齟齬をなくしていくのは大事。

記入された付箋の数々

時間内で全てについて議論はできなかったものの、各章について以下のような意見・感想がそれぞれのメンバーから出たので紹介いたします。

第17章 アジャイルとリーンの概要

第18章 テスト自動化の戦略

第19章 プランニングポーカーによる見積もり

第20章 因果関係図

第21章 最後に伝えたいこと

最後に

本を実際に輪読する会は今回が最後でしたが、これまで話し合ったなかで良さそうだと感じた取り組みを組織内で実践してみて、どんな学びがあったか振り返る会が予定されています。次回はその振り返りの様子をブログでお届けいたします。 最後まで読んでいただきありがとうございました!

仮説→実験→検証→学び...プロダクト開発のループを実現するために行っていること

こんにちは! @TOC@takapy です。

最近は期の終わりも近づいてきたので、普段利用しているREALFORCEのキーボードを大掃除しました。めっちゃ綺麗になって気分も一新できたので、定期的にやらねばと思いました。いつもありがとうREALFORCE!

さて、今回はプロダクト開発においてMobius Outcome Deliveryの思想を取り入れた話をご紹介しようと思います。

私は7月にこのMobius Outcome Delivery研修を受け、実際にこの考えをプロダクト開発に取り入れてみることでプロダクト開発をする上で感じていた課題感を解消できるヒントになるのではないか、と思いました。

Mobius Outcome Deliveryとは何なのか、またその思想を実際にプロダクト開発においてどう取り入れたのかの具体的な事例について興味ある方の参考になれば幸いです。


目次


Mobius Outcome Deliveryとは

Mobius Navigator(https://www.mobiusloop.com/)

www.mobiusloop.com

Mobius Outcome Deliveryは価値を生み出すためのナビゲーターであり、戦略からデリバリーまでを繋ぎ、見えるようにするフレームワークです。「最小限のアウトプットで最大限のアウトカムを達成する」ことをゴールとしており、研修でも度々 アウトカム(価値) という単語が登場しました。 アイデアをいかにシンプルに小さく実験するか、を問いながら上図のようなメビウスの輪の形状をしたMobius Navigator(メビウスナビゲーター)に沿ってプロダクト開発を進めていきます。

プロダクト開発のループを大きくDiscover, Decide(Options), Deliverの3セクションで表現しており、プロダクト開発における羅針盤的なものになり得るフレームワークになっております。

弊社では「プロダクト開発は作って終わりではない」とよく言われています。Mobius Outcome Deliveryはその言葉を表現できるフレームワークである点が一番気に入っています。 ユーザーに届けて終わりではなく、届けた結果どうなったのか、そこから学びを得て次に何をやるのか、を意識できるようになっており、プロダクト改善におけるループを辿っていけるフレームワークとなっております。

当時の課題感、何がやりたかったのか

弊社では現在、アジリティが全社におけるキーワードとなっており、チームでもいかに小さく実験していけるか、が関心ごととして強くなっています。

その際に「仮説を立て、実験し、検証する」のサイクルを回しているのですが、下記のような課題感を感じておりました。

  • 各実験が点になっていて繋がっていないのではないか、繋がっていたとしてもそれが見えていないのではないか
  • なんとなく進みが遅いと感じるが、どこがボトルネックになっているのか特定がしづらいのではないか
  • やったことがチーム内に閉じていて、その知見をチーム間や将来の誰かのために活かせられてないのではないか

当時、プロダクトマネジメント本の輪読会を行ったメンバーとプロダクト開発におけるプロセスの改善を行おうと思ったときに課題感を擦り合わせたのですが、上記3点がほぼ同じ課題感として挙がっており、「仮説→実験→結果→学び→仮説 … のループを回したい、それを見える化したい」という共通の思いが一致したので、これを実現できる方法を模索することになりました。

解決に向けて、方法の模索

Mobius Outcome Deliveryが仮説→実験→結果→学び→仮説 … のループをうまく表現していると思ったので、この研修で得たものを共有し、どうやってこの思想を表現できるか、プロダクト開発に取り入れられるか、を考えてみました。

そこでこの思想をプロダクト開発に取り入れるためのツールとして、 Miro など色々な方法を模索したのですが、Notionのプロジェクト管理テンプレートが一番やりたいことを実現できるのではないか、と思いました。

www.notion.so

Notionのプロジェクト管理は1つのプロジェクトに対して複数のタスクが紐づく形で構成されています。

Notion Projectについて

これを弊社のプロダクト開発に応用して、1つのやりたいこと、達成したい価値に対して実験を紐づけることでやりたいことができるのではないか、と考えました。

実際に作成したものをもとにどう実現したのかを具体的に説明します。

Mobius Outcome Deliveryを実現するために、どうやってNotionを活用したのか

以下Mobius Outcome Deliveryを実現する上でポイントになる点に絞って説明していきます。

Discover:テーマを作成する

Discoverの道のり

まずは上図の左側、Discoverの部分です。ここでは「なぜやるのか、誰に向けてやるのか」といった問題設定を行い、それを解決した際に得られるアウトカム、を決定します。

この部分をNotion Projectsにおける プロジェクト として作成します。これを自分達は テーマ と呼ぶようにしました。

データベースから新規テーマを作成すると下記のようなテンプレートが表示されます。

テーマのテンプレート

このテンプレートを埋めていくことでMobius Outcome Deliveryの左側Discoverを辿っていく形式になります。

Options, Deliver:実験を作成する

Options, Deliverの道のり

実際にやりたいことが定まったら、実現するための実験を考えていきます。

どうやったら一番シンプルに簡単に実験できるかを考え、やることが決まったら実験を作成します。

実験の作成

実験を作成するボタンを押すと、このテーマに紐づいた実験が作成されます。

実験はテーマが紐づいた状態で作成されるので、この実験は何を実現するためのものなのか、といった紐付けが見えるようになります。

このドキュメントは弊社では実験ドキュメントと呼ばれており、どうやって実験するのか、この実験で得た学び、などを記載できるようになっています。

この実験ドキュメントについては後の章で詳しく説明します。

テーマと実験の紐付け

実験が終わり、学びを得たら、次の一手を決めていきます。

Decide:もっと作るのか、別の問題を見つけるのか

Decideの道のり

実験をしても必ずしもうまくいくとは限りません。やってみて実際に得た反応をもとにもっと作っていくのか、はたまた別の問題を見つけるのか、といった選択をする必要があります。

これはMobius Outcome DeliveryでいうDecideのフェーズであり、ここで分岐が発生します。 この次への繋がりが一番実現したかった部分であり、今回のこだわったポイントでもあります。

私たちはこの繋がりをNotionのリレーションを用いて表現しました。

1. もっと作るという判断をした場合

例えば実際にユーザーに使ってもらい反応をみた際に、ニーズはありそうだが、少し別の形で実装をしてみたい、と思ったとします。Mobius Navigatorでいうところの再度Createのループに入るイメージです。

もっと作る判断をした場合

この時は実験ドキュメントで「次の実験を作成する」ボタンを押します。

次につながる実験を作成する

これを押すと2回目の実験が作成され、 前の実験 プロパティに1回目の実験とのリレーションが作成されます。

このリレーションにより2回目の実験は何を根拠に生まれた実験なのか、の繋がりを表現できるようになりました

実験の繋がり

2. 別の問題を見つけるという判断をした場合

実際にユーザーに届けてみるとニーズがないことがわかったり、実験をすることで別の問題を見つけることもあります。次に繋がる実験だったということですね。

この時Mobius Navigatorでいうところの再度Discoverのループに入るイメージです。

別の問題を見つけた場合

テーマのドキュメントに戻るとネクストアクションが設定されています。ここで「次のテーマを作成する」ボタンを押すと、次に繋がるテーマが作成されます。

次に繋がるテーマを作成する

このリレーションが作成することで、テーマ間での繋がりも作成されます。

こうすることで下図のように1つ1つのループにおける繋がりも表現できるようになりました。

ループが繋がっていくイメージ

まとめると

テーマと実験の関係図をまとめると下記のようになります。

テーマと実験の関係図

また、一連の流れをMobius Navigatorのループに当てはめると下記のようになります。

Mobius Navigatorに沿ってやること

このようにNotionを利用することで、Mobius Outcome Deliveryのフローをできるだけ簡単に、かつテンプレートに沿っていくことで実現できるような仕組みを作成しました。

最終的に作成したものはコネヒトカンバンと命名し、PdMを中心に展開をしていきました。

コネヒトカンバン爆誕

補足: 実験ドキュメントについて

ここで、先程説明をスキップした実験ドキュメントの詳細について改めて紹介します。

上記で説明した「テーマ」に紐づく形で、「実験ドキュメント」が作成されます。

前述した通り、この実験ドキュメントは1つのテーマに対して複数紐づくものとなっています。また、実験同士の繋がり(前後関係)も表現することができます。

実験ドキュメントのテンプレートは、以前のブログでも紹介した「A/Bテスト標準化テンプレート」を土台として、少しブラッシュアップして作成しました。

tech.connehito.com

テンプレートに入れ込んだ最終的な項目は以下のようなものです。

  • 実験の背景
  • 実験の概要
  • モニタリング指標
  • 実験後のアクションプラン
  • 実験結果

以降ではブラッシュアップしたポイントを中心に紹介します。

ステータス管理を容易に

冒頭で述べた通り、課題感の1つに「なんとなく進みが遅いと感じるが、どこがボトルネックになっているのか特定がしづらいのではないか」というものがありました。

そこで

  • 各実験は今どのステータスなのか
  • ステータスが暫く変わっていないのであれば、何が原因で変わっていないのか

などの現状を知り、そこでボトルネックを共有して解決を図れるような仕組みにできるよう、ステータス管理を簡単に行えるようにしました。

Notionで以下のようなテンプレートを用意しておき、「開発開始」や「検証開始」などのボタンを押すだけで、ステータスが変更されるようにしました。(ボタンのロジックなどは後述)

ステータス変更を簡単にするためのボタン

テンプレートの末尾には、実験終了したタイミングでステータス更新 & 結果記入欄が表示されるボタンも追加しています。

実験が終了した際のボタン

実験のサマリを把握しやすく

実験ドキュメントは現在の状態を把握できるだけでなく「過去に何がうまくいって、何がうまくいかなかったのかを振り返ることができる」ドキュメントとしても効果を発揮します。

過去の実験を一覧で見たときに「どの実験がどんな目的で行われて、結果はどうだったのか」がパッと分かるとハッピーですよね。

そこで、実験のサマリをNotionのページプロパティに持たせることで、一覧で見たときに逐一ドキュメントの中身を開かずとも大まかな内容を知ることができるようにしました。

実験サマリプロパティのテンプレート

一覧で見るとこんな感じです(マスク部分が多くて分かりづらいかもしれませんが雰囲気だけでも感じていただければと)

実験一覧

カンバンを作成する上で工夫した点

以上が大まかなカンバンの仕組みと詳細になります。

今回このコネヒトカンバンを作成するにあたって、実際に使ってもらうようにする工夫をいくつか行ったのでご紹介します。

利用者はできるだけ流れに沿うだけで利用できるようにする

今回カンバンを作成するにあたって、とにかく流れに沿えば自動的にメビウスループに乗っているような設計を心がけました。

その際に役に立ったのがNotionのボタン機能です。

www.notion.so

この機能を使うことで、ボタンをクリックした時にプロパティの操作やページ作成などを自動で行ってくれます。

例えば「次のテーマを作成する」ボタンはクリックすると下記のような動作を自動でおこなってくれます。

操作するページのステータスを 完了 にする→繋がりを紐づけてページを作成する→新規ページを開く

ボタンを使ったプロパティの変更設定

このように利用者はボタンを押すだけで自然とループに乗っている状態を目指すことで利用側のハードルをできるだけ下げるようにしました。

目的や思想、使い方のドキュメントを用意する

今回カンバンを作るにあたって、目的や思想の設計を入念に行いました。

実際にこのカンバンを作った人はいいのですが、利用する人たちはやりたいことなどを理解しないと「これをやる目的ってなんだろう…」といった状態になってしまいます。

なので、このカンバンの目的やどういった思想のもとで作成しているのかを書いたドキュメントを用意しておきました。

カンバンの目的を書いたドキュメントの一部

ただ、用意しているだけでちゃんと読んでもらえるとは限りません。そこは読んでもらう工夫だったり、仕組み化を今後も考える必要があると思っています。

実際に運用してみてどうだったか

PdMを中心に実際に利用してもらい、数ヶ月が経ちました。実際に使ってみた感想をアンケートで取ってみたので一部ご紹介します。

全チームの状況が一箇所で見れるのが最高

ステータスが俯瞰でみれるのがけっこう好き

特に繋がりの部分はポジティブな意見が多かったです

前の繋がりがあるから文脈思い出しやすい(あーこのゴールに向かう施策ね〜的な)

前後の繋がりが圧倒的にわかりやすくなった!

みんな書いてるけど、つながりがみえるようになったのはとても良い!

嬉しい声もありつつ、下記のような課題感もあらためて発見できました。

説明されると理解できるが、自分だけだと構造や使い方を理解するのにハードルがあった

実験というワードが強く、1回きりの施策の時(実験じゃない場合)に書いていいのかちょい悩むことがある

前後のつながりが見えるようになったのはとても良いが、まだ運用して時間が経っていないので本当に効果的かは長い目で見たい

ある程度想定はしていたり、対策をしたつもりでしたが、やはりこの辺は難しいですね。

浸透に関しては辛抱強く、でも楽しみながらみんなで続けていって、より良いものになるようにしていきたいと思ってます!

今後の展望

このカンバンを導入して、約2ヶ月ほどが経過しました。 色々書きましたが、まだ道半ばであり実験的な取り組みです。

改めて自分達がやりたかったこと、達成したかったことができてるかと言われると、できつつあるけど、まだまだこれから、と言う状態です。なので、今後もブラッシュアップしていきたいと思っています。

例えば、実験で得た知見をPdMやチーム間で共有できるような仕組みを用意したり、もっと過去の繋がりを遡って追加したり…などなど。

やりたいことがたくさんありすぎ状態ですが、同時にワクワクもしているのでこの取り組みを今後も続けていって、Mobius Outcome Deliveryの知見と実践を積んでいきたいと思います。

並行処理と並列処理の違いをコンテキストから考える

こんにちは!バックエンドエンジニアのjunyaUと申します。

実は9月入社で入社してから1ヶ月も経っていない(執筆当時)のですが、テックブログを書かせていただけるのは本当にありがたい環境だな〜とヒシヒシと感じております。

最近、プライベートではもっぱら自作OSの開発をしており、時間があっという間に溶けてしまいご飯をちゃんと食べれていないのが悩みです。

今回は並行処理と並列処理の違いをコンテキストの観点から考えて、両者がどのように違うのかを考察していこうと思います〜!

はじめに

なぜ記事を書こうと思ったのか

数年前、私が学生だった頃に初めてGoを触り、そこで「並行処理」という言葉を初めて耳にしました。その言葉を初めて聞いたとき、「プログラムを同時に動かすことができるのか!」とワクワクしましたが、並行処理について調べてみると、

  • 実は高速に切り替えて実行しているだけで本当に同時に実行していない
  • 並列処理と並行処理があり、並列処理は本当に同時に実行している

というような内容が書かれており、当時は全く理解できずぼんやりとしたまま考えるのをやめてしまいました。

時は過ぎ、私はOSの自作やコンピューターサイエンスの勉強にハマりました。

そこで、プロセスの並行処理を調べたり自分で実装したりするうちに並行処理と並列処理への解像度が少し上がりました。

ネットで並列処理と並行処理の違いを調べてみても、この問題を考える上で重要なコンテキストやコンテキストスイッチの概念に触れている記事が少なかったので、今回はコンテキストの観点から両者の違いを考えていこうと思います。

並行処理と並列処理の違いの結論

結論から述べると、プロセスAとプロセスBの実行に際して、

  • 並行処理は、特にシングルコアの場合、1つのコアがAとBのコンテキストを高速に入れ替えながらプロセスを実行する方式
  • 並列処理は、複数コアを使って、AとBのプロセスを真に同時に実行する方式

となります。

この違いを理解するためには、コンテキストの概念の理解は不可欠です。

次節からコンテキストに焦点を当てて並行処理と並列処理の違いを見ていきます。

プロセスのコンテキストとは?

コンテキストの定義

コンテキストとは、プロセスの現在の実行状態を表す情報の集合を指します。

これは、プロセスが中断された後、その状態から継続して実行できるようにするための「状態のスナップショット」であると言えます。

実際には、プロセスの実行状態を保存するためのデータは様々ありますが、重要でわかりやすいデータとしては、以下のものがあります。

  • プログラムカウンタ : 次に実行する命令が格納されているアドレス
  • スタックポインタ : 変数や一時的な計算結果など、プログラムの実行に必要なデータが格納されているアドレス
  • フラグレジスタ:条件分岐や算術命令の結果に基づくフラグの値

これらのデータの多くがCPUのレジスタに格納されています。

なので、コンテキストとはレジスタの内容 と考えることができます。

では、このレジスタについて詳しく見ていきます。

レジスタとは?

CPUとメモリの簡略図

レジスタはCPUの内部に存在する、高速にアクセス可能である小さな記憶領域です。

CPUはメインメモリに直接アクセスするよりもレジスタへのアクセスの方が高速であるため、頻繁に使用されるデータや、実行中の命令の情報は一時的にレジスタに格納されます。

CPUの動作は、基本的に「命令のフェッチ」と「命令の実行」の繰り返しで、フェッチする際はメモリからデータや命令を取得し、レジスタに格納され、レジスタの値から処理が行われます。

このレジスタには先ほど述べた、プログラムカウンタやデータなどが格納されているわけですが、

もしプロセスAの実行中に、プロセスBのデータのアドレスやプログラムカウンタの情報でレジスタの内容を上書きした場合、どうなるでしょうか?

プロセスAの実行中にも関わらずいきなりプロセスBの実行に切り替わってしまいます。

これが、並行処理で行われていることの核心となる部分です。

次の節で詳しく見ていきます。

並行処理とは?

並行処理の定義

並行処理の定義は、「1つのCPUコアに対して複数のコンテキストを高速に入れ替えながらプロセスを実行する方式」と冒頭で述べました。

改めて、コンテキストにフォーカスして考えてみます。それぞれのプロセスはそれぞれのコンテキストを持っています。(メモリに格納されている場所やデータがそれぞれ違うので当たり前ですね)

メモリの簡略図(アドレスや配置は説明用なのでデタラメです)

上の簡単な図をもとに考えてみます。

プロセスAのコンテキスト

  • プログラムカウンタ: 0x1000
  • スタックポインタ: 0x3000

プロセスBのコンテキスト

  • プログラムカウンタ: 0x7000
  • スタックポインタ: 0x9000

プロセスAが実行され始めた時、レジスタにはプログラムカウンタの0x1000、スタックポインタの0x3000を始めとした様々な値が保存されています。

ここで、プロセスAの実行中に現在のレジスタの値をどこかに退避させ、プロセスBのコンテキストをレジスタに格納すると、プロセスAの処理が中断され、プロセスBに処理が切り替わります。

これを連続して高速に行うとどうなるでしょうか?

CPUから見ると、複数のプロセスを高速に1つずつ処理しているだけなのですが、

人間から見ると、プロセスAとプロセスBが同時に動いているように見えると思います。

これが並行処理なのです。

コンテキストスイッチ

コンテキストスイッチの簡略図

並行処理について調べていると「コンテキストスイッチ」というワードを目にすることがあると思います。当時の私にとってはこれが難しく理解できませんでした。

ですが、コンテキストスイッチの説明は既に上の節で述べられています。

プロセスAの実行中に現在のレジスタの値をどこかに退避させ、プロセスBのコンテキストをレジスタに格納すると、プロセスAの処理が中断され、プロセスBに処理が切り替わります。

この部分です。レジスタの値をプロセスAのコンテキストからプロセスBのコンテキストに切り替えるという処理がコンテキストスイッチとなります。

もう少し砕いた言い方をすると、コンテキストスイッチ = レジスタの値の入れ替え

ということがいえます。

また、高速にコンテキストスイッチを行うことが並行処理といえます。

並列処理とは?

並列処理の定義

並列処理の定義は、「並列処理は複数コアを使って、AとBのコンテキストをそれぞれ異なるコアに格納し、複数のプロセスを真に同時に実行する方式」と冒頭で述べました。

並行処理は1つのCPUコアが複数のプロセスを切り替えながら実行するものなので、実際には同時実行していませんが、並列処理は真に同時に実行しています。

なぜ並列処理は真に同時に実行することができるのでしょうか?

コンテキストとマルチコア

並列処理の真髄は、CPUが複数のコンテキストを同時に扱える能力にあります。

コンテキストの複数保持を実現してくれるのがマルチコアの存在です。

CPUコアはコアごとにそれぞれレジスタを持っており、複数のコンテキストを同時に保持、実行することができます。

プロセスAとプロセスBを並列実行している簡略図

並行処理はコア1つに対してAとBを同時に実行させようとしていたので、高速にAとBをコンテキストスイッチする必要があったのに対して、並列処理は複数のコアを用いるのでコンテキストスイッチをすることなくそれぞれのコアにコンテキストを保持させて真に同時に実行することができます。

まとめ

並行処理は1つのコアに高速にコンテキストを切り替えて実行していく方式で、

並列処理は複数コアが、同時に実行したいプロセスのコンテキストをそれぞれ保持して同時に実行をしていく方式でした。コンテキストからのアプローチで見ると思っていたより理解しやすいのではないでしょうか。

実際には、どちらの処理方式も単独で使用されることは少なく、一般的にはこれらを組み合わせて利用されます。今回は、それぞれの方式の懸念点やパフォーマンス上のトレードオフ、最適な使い所には触れていませんが、これらのテーマは非常に奥が深いので、機会があればまた書きたいなと思います。

なお、今回の説明はわかりやすさを重視して簡略化している点もあるため、その点をご了承ください。

Lambda も Chatbot も SNS も使わない!EventBridge だけで CloudWatch Alarm のメッセージを加工し Slack へ post する方法

こんにちは。開発部プラットフォームグループでインフラエンジニアをしている @sasashuuu です。先日、Custom notifications are now available for AWS Chatbot で AWS Chatbot でのカスタム通知が行えるアップデートが発表されました。 実はこの方法以外にも、AWS EventBridge だけで CloudWatch Alarm のメッセージをさらに柔軟に加工して Slack へ post する方法があることをご存知でしょうか。Chatbot のアップデートが発表される前に当社で導入していたのですが、SNS や Chatbot を組み合わせたアーキテクチャを組まなくても良い他、 Lambda のランタイムの管理や実装の煩雑さのリスクなども減るので意外と活用できる方法なのではないかと思っております(実は当時 Chatbot を利用した上でメッセージ加工ができないかを模索した結果、できないことを知りこの方法に辿り着きました。そしてその方法を紹介しようとこのブログの8割ほどを執筆し終えた時に Chatbot のアップデートが発表され、なんとも言えない気持ちになったことは内緒です...)。この記事ではその方法についてご紹介したいと思います!

Before/After

まずメッセージの Before/After からお見せします。

ECS のタスク数が一定時間に必要な数から基準を下回った際に発生するアラートを例にご紹介します。

Before

Before の状態は Chatbot を通じて post されたデフォルトのメッセージとなっています。内容は充実しているのですが、Slack 通知をする上では情報過多な印象で、見慣れていなければどういった内容のアラートなのかを瞬時に把握し、対応するのはハードルが高いような印象でした。

After

After の状態は EventBridge を通じて加工されたデフォルトのメッセージとなっています。

メッセージの変更内容ですが、以下のような点を工夫しました。

  • 端的に何を示すアラートなのかを日本語で表現
  • 弊社の現場において Slack で通知する上で不要と判断した情報を除去
  • アラートに対するアクションを促せるような文言を追加

アーキテクチャ概要

タイトルや冒頭でも触れていましたが、AWS EventBridge を使用します。

重要となる構成要素は以下のラインナップです。

  • イベントパターン
    • AWS で発生したイベントを検知するためのパターン
  • ルール
    • AWS サービスのイベントを検知しターゲットへ送信するためのリソース
  • 入力トランスフォーマー
    • ターゲットへ渡すためのイベント(テキスト)をカスタマイズできる機能
  • ターゲット
    • 対象のイベントおよびパターンにマッチした際に送信先となるエンドポイント
  • API Destination
    • ターゲットに HTTP エンドポイントを指定できる機能

上記を組み合わせて作成した処理の流れをざっくりと説明すると以下のようになります。

実装手順

ここからは具体的な実装手順について解説します。構築時はコンソールでの作業を中心に進めていましたが、ここでは最終的に IaC へ落とし込んだ Terraform のコードをメインに解説させていただきますのでご了承ください。

Slack App を作成

Slack App が必要になるので、なければ作成をしてください。

https://api.slack.com/apps へアクセス後、下記の動線から作成が行えます。

Incomming Webhooks の作成

Incomming Webhooks を使用しますので、作成し取得しておきます。

下記の動線から作成&取得可能です。

書き込み権限の追加

Slack App へ chat:write の権限を追加します。

下記の動線から設定可能です。

Terraform の実装

下記はルールに関する定義です。

resource "aws_cloudwatch_event_rule" "demo_alert_alarm" {
  name = "demo-alert-alarm"
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
        }
      ],
      detail = {
        state = {
          value = [
            "ALARM"
          ]
        }
      }
    }
  )
}

マッチさせたい条件を HCL に記述し、event_pattern の値に定義しています。

下記はターゲットに関する定義です。

resource "aws_cloudwatch_event_target" "demo_alert_alarm" {
  arn      = aws_cloudwatch_event_api_destination.demo_alert.arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.demo_alert_alarm.id

  input_transformer {
    input_paths = {
      account     = "$.account"
      alarmName   = "$.detail.alarmName"
      description = "$.detail.configuration.description"
      reason      = "$.state.reason"
      region      = "$.region"
      time        = "$.time"
    }
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "#E01D5A",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "*<alarmName> - ECSの必要なタスク数が足りていません*"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "アラートの内容に注意してください。\\n必要であれば *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ>* から「 <alarmName> 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針>* を参考に今回の対応記録も残しましょう。)"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

input_transformer では、local.input_path_settings で local values の EventBridge の変数用の定義を指定し、input_template では post する Slack のメッセージのヒアドキュメントを定義しています。このヒアドキュメントの中で、EventBridge の変数を のように使用し、メッセージで展開が行えます。

下記はターゲットに付随するリソースに関する定義です。

resource "aws_cloudwatch_event_api_destination" "demo_alert" {
  connection_arn                   = aws_cloudwatch_event_connection.demo_alert.arn
  http_method                      = "POST"
  invocation_endpoint              = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" # webhook URL はここでは管理しない
  invocation_rate_limit_per_second = 300
  name                             = "demo-alert"
  lifecycle {
    ignore_changes = [
      invocation_endpoint
    ]
  }
}

resource "aws_cloudwatch_event_connection" "demo_alert" {
  authorization_type = "API_KEY"
  name               = "demo-alert"
  auth_parameters {
    api_key {
      key   = "Authorization"
      value = "dummy"
    }
  }
}

aws_cloudwatch_event_api_destination, aws_cloudwatch_event_connection では イベントの送信先と接続設定に関する設定を行なっています。aws_cloudwatch_event_api_destination の invocation_endpoint には Webhook URL の値が設定されます。aws_cloudwatch_event_connection の auth_parameters.api_key の設定は使用しないため、適当な文字列の設定で大丈夫です。また、注意点としてここでは説明をわかりやすくするために、invocation_endpoint をマスクした値でハードコードしているような実装になっていますが、セキュアな情報となりますのでこの辺りの管理や参照などは適宜安全な方法で行ってください。

最後は IAM に関する定義です。

EventBridge が ApiDestination へイベントを送信するための権限などを設定しています。

resource "aws_iam_role" "demo_alert" {
  assume_role_policy = jsonencode({
    "Statement" : [
      {
        "Action" : "sts:AssumeRole",
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "events.amazonaws.com"
        }
      }
    ],
    "Version" : "2012-10-17"
  })
  managed_policy_arns = [
    aws_iam_policy.demo_alert.arn,
  ]
  max_session_duration = 3600
  name                 = "demo-alert"
}

resource "aws_iam_policy" "demo_alert" {
  name = "demo-alert"
  policy = jsonencode({
    "Statement" : [
      {
        "Action" : [
          "events:InvokeApiDestination"
        ],
        "Effect" : "Allow",
        "Resource" : [
          "arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:api-destination/demo-alert/*"
        ]
      }
    ],
    "Version" : "2012-10-17"
  })
}

ここまで解説した内容の実装は、CloudWatch Alarm の state だと ALARM に関連する内容のものとなっています。ですが、実際にはアラートが復旧した場合の OK 通知も必要となる場合がほとんどでしょう。そのため下記のリソースは state ごとに 1セットで作成するので必要に応じて追加してください。(その他は共用で利用)

  • aws_cloudwatch_event_rule
  • aws_cloudwatch_event_target

最終的に出来上がった全体のコードはこちらになります。

resource "aws_cloudwatch_event_rule" "demo_alert_alarm" {
  name = "demo-alert-alarm"
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
        }
      ],
      detail = {
        state = {
          value = [
            "ALARM"
          ]
        }
      }
    }
  )
}

resource "aws_cloudwatch_event_rule" "demo_alert_ok" {
  name = "demo-alert-ok"
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
        }
      ],
      detail = {
        state = {
          value = [
            "OK"
          ]
        }
      }
    }
  )
}

resource "aws_cloudwatch_event_target" "demo_alert_alarm" {
  arn      = aws_cloudwatch_event_api_destination.demo_alert.arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.demo_alert_alarm.id

  input_transformer {
    input_paths = {
      account     = "$.account"
      alarmName   = "$.detail.alarmName"
      description = "$.detail.configuration.description"
      reason      = "$.state.reason"
      region      = "$.region"
      time        = "$.time"
    }
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "#E01D5A",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "*<alarmName> - ECSの必要なタスク数が足りていません*"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "アラートの内容に注意してください。\\n必要であれば *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ>* から「 <alarmName> 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針>* を参考に今回の対応記録も残しましょう。)"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

resource "aws_cloudwatch_event_target" "demo_alert_ok" {
  arn      = aws_cloudwatch_event_api_destination.demo_alert.arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.demo_alert_ok.id

  input_transformer {
    input_paths = {
      account     = "$.account"
      alarmName   = "$.detail.alarmName"
      description = "$.detail.configuration.description"
      reason      = "$.state.reason"
      region      = "$.region"
      time        = "$.time"
    }
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "#2DB57C",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "*<alarmName> - ECSの必要なタスク数が足りていません*"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "引き続きアラートに注意してください"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

resource "aws_cloudwatch_event_api_destination" "demo_alert" {
  connection_arn                   = aws_cloudwatch_event_connection.demo_alert.arn
  http_method                      = "POST"
  invocation_endpoint              = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" # webhook URL はここでは管理しない
  invocation_rate_limit_per_second = 300
  name                             = "demo-alert"
  lifecycle {
    ignore_changes = [
      invocation_endpoint
    ]
  }
}

resource "aws_cloudwatch_event_connection" "demo_alert" {
  authorization_type = "API_KEY"
  name               = "demo-alert"
  auth_parameters {
    api_key {
      key   = "Authorization"
      value = "dummy"
    }
  }
}

resource "aws_iam_role" "demo_alert" {
  assume_role_policy = jsonencode({
    "Statement" : [
      {
        "Action" : "sts:AssumeRole",
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "events.amazonaws.com"
        }
      }
    ],
    "Version" : "2012-10-17"
  })
  managed_policy_arns = [
    aws_iam_policy.demo_alert.arn,
  ]
  max_session_duration = 3600
  name                 = "demo-alert"
}

resource "aws_iam_policy" "demo_alert" {
  name = "demo-alert"
  policy = jsonencode({
    "Statement" : [
      {
        "Action" : [
          "events:InvokeApiDestination"
        ],
        "Effect" : "Allow",
        "Resource" : [
          "arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:api-destination/demo-alert/*"
        ]
      }
    ],
    "Version" : "2012-10-17"
  })
}

additional

ここまでは基本的な実装方法をお伝えしましたが、対象の CloudWatch Alarm リソースが増える度に定義が増えてしまう冗長な設計となっていました。応用として下記のように local values に CloudWatch Alarm ごとの固有の設定に関する定義を切り出し、loop させて DRY に実装する方法もおすすめです。ここでは詳しくは解説しませんが、一部のサンプルコードをご紹介しておきます。

locals {
  input_paths = {
    account     = "$.account"
    alarmName   = "$.detail.alarmName"
    description = "$.detail.configuration.description"
    reason      = "$.state.reason"
    region      = "$.region"
    time        = "$.time"
  }

  config = [
    {
      target_arn     = aws_cloudwatch_event_api_destination.demo_alert.arn
      name_prefix    = "demo-alert"
      prefix_pattern = "arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev"
      alert = {
        alarm = {
          main_message = "*<alarmName> - ECSの必要なタスク数が足りていません*"
          sub_message  = "アラートの内容に注意してください。\\n必要であれば *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ>* から「 <alarmName> 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *<https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針>* を参考に今回の対応記録も残しましょう。)"
          color        = "#E01D5A"
          state_type   = "ALARM"
        }
        ok = {
          main_message = "*<alarmName> - ECSの必要なタスク数が足りていません*"
          sub_message  = "引き続きアラートに注意してください"
          color        = "#2DB57C"
          state_type   = "OK"
        }
      }
    }
  ]
}

resource "aws_cloudwatch_event_rule" "demo_alert" {
  for_each = {
    for config in local.config : config.name_prefix => {
      name_prefix    = config.name_prefix
      prefix_pattern = config.prefix_pattern
      alert          = config.alert
    }
  }

  name = format("%s-%s", each.value.name_prefix, "alarm")
  event_pattern = jsonencode(
    {
      source = [
        "aws.cloudwatch"
      ],
      detail-type = [
        "CloudWatch Alarm State Change"
      ],
      resources = [
        {
          prefix = each.value.prefix_pattern
        }
      ],
      detail = {
        state = {
          value = [each.value.alert.alarm.state_type]
        }
      }
    }
  )
}

resource "aws_cloudwatch_event_target" "demo_alert" {

  for_each = {
    for config in local.config : config.name_prefix => {
      name_prefix    = config.name_prefix
      prefix_pattern = config.prefix_pattern
      alert          = config.alert
      target_arn     = config.target_arn
    }
  }

  arn      = each.value.target_arn
  role_arn = aws_iam_role.demo_alert.arn
  rule     = aws_cloudwatch_event_rule.alarm[each.value.name_prefix].id

  input_transformer {
    input_paths    = local.input_paths
    input_template = <<EOF
    {
  "attachments": [
      {
          "color": "${each.value.alert.alarm.color}",
          "blocks": [
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "${each.value.alert.alarm.main_message}"
                  }
              },
              {
                  "type": "section",
                  "fields": [
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarm名:*\n<alarmName>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*CloudWatch Alarmの詳細:*\n *<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/<alarmName>?|AWS Console URL>*"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*AWSアカウント:*\n<account>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*リージョン:*\n<region>"
                      },
                      {
                          "type": "mrkdwn",
                          "text": "*発生日時:*\n<time>"
                      }
                  ]
              },
              {
                  "type": "section",
                  "text": {
                      "type": "mrkdwn",
                      "text": "${each.value.alert.alarm.sub_message}"
                  }
              }
          ]
      }
  ]
}
    EOF
  }
}

おわりに

今回は、EventBridge で CloudWatch Alarm のメッセージを加工し、Slack へ post する方法をご紹介しました。Lambda などを使わずとも柔軟にメッセージを加工できるのは個人的に発見でした。皆さんも機会があればぜひ試してみてください!

Go言語による並行処理の輪読会で行った、輪読会でワイワイするための工夫

こんにちは!@TOC です! 最近は「ゾン100〜ゾンビになるまでにしたい100のこと〜」というアニメにハマっており、見るたびに「やりたいことをやるんだ!」という気持ちになります🙌 キャラも魅力的なのでこれからが楽しみですね!

さて、今回は社内で行った「Go言語による並行処理」本の輪読会が最近完走したので、その様子をご紹介しながら、輪読会をより良いものにするために行った工夫点についてご紹介します。


目次


輪読会で読む本の選定

今回はオライリーから出版されている「Go言語による並行処理」という本を読む本として選定しました。

www.oreilly.co.jp

Goに関する本は数多く出版されていますが、この本はGo言語における並行処理に特化した本で、Goの並行処理を学ぼうと思った人は一度目にしたことがあるのではないでしょうか。

数あるGoの本の中からこの本を選定した理由は2つあります。

  1. 本の内容として難易度が高く、一人で読むと心が折れそうだから
  2. Goの強みでもある並行処理を業務で活かせるレベルに引き上げたいから

一つ目は少し弱気な理由にはなりますが、今回輪読会をやった感想としてはかなり大事なポイントかなと思っております。「Go言語による並行処理」本はかなり丁寧に解説されてるので、読んだらなんとなくわかった気になりますが、理解を深める・自分の中で昇華するまで一人で行うにはかなり骨が折れます。

以前、GoのLT会でこの本が紹介された時も「一人で読んで心が折れた」と言った意見が多く見られました。

その点、輪読会で一緒に読む仲間がいると進めようという気持ちも高まりますし、議論することで理解が深まりやすいです。なので、この本に限らず、興味あるけどなかなか読み進められない本は一緒に読む仲間を見つけるのは一つ良い方法かなと思います。

二つ目に関しては、現在コネヒトではテックビジョンとしてLet's Goというものを掲げております。

tech-vision.connehito.com

新たな武器としてGoを選定したわけですが、せっかくならGoの強みである並行処理についてちゃんと理解したい、業務で使えるようになりたい、という思いもあり、この本を選定しました。

どういう形式でやったのか

頻度としては隔週くらいで行いました。事前に読む章を決めて読んでおき、学びになったこと、疑問に思ったことを付箋に書いて各付箋について議論していきます。

輪読会の様子
わからない箇所について議論をしたり、後述するようにコードを実際に動かしてみて理解を深めたりしました。

こうやって議論をすることで一人だけでやるよりも理解を深められるのが輪読会のいいところですね!

輪読会を進める上での工夫

今回輪読した本は丁寧に書いてありつつも、理解に至るには難易度も高く進めるのも一苦労でした。そのため輪読会を進める上で工夫した点を紹介できればと思います。

1. 写経当番を決めて、実際に手を動かす

「Go言語による並行処理」は3章くらいからコード表記が多くなり、実践的な内容になってきます。並行処理は実際に動かしてみたり、コードを変えてみたりして挙動を確かめないと全然動きが想像できなかったりするので、3章以降では各パートで写経当番を決めて、各自写経してくることにしました。

写経したコードをコミットできるリポジトリを用意し、自分の担当した分のコードをコミットしていきます。

これは実際にコードを動かしながら学べるし、負担を分担できるため今回のケースとしてはかなり有用だったかなと思います。みんなやってるんだから自分もやらなきゃって気持ちになりますしね!

やはりある程度イメージしづらい内容だとコードで語る方が理解しやすくなる側面があるかと思うので、難易度が高かったりイメージしづらい内容の輪読会の場合、この方法はおすすめです。

2. 学んだことを実践的にアウトプットしてみる勉強会でワイワイ

輪読会を完走し、実際に写経しながら進めていたとはいえ、やはり 知っている使える には大きなギャップがあります。なので、テーマはなんでもいいから、実際に並行処理を書いてみてみんなでワイワイする勉強会を行おうという話になりました。

せっかくならガッツリやって、最後みんなで飲みにでも行こうという話になったので、有志で日曜日に会社に集まって、各々がテーマを決めて並行処理を書いてみる会を行いました。

テーマは以下のようなものがありました

  • PHPのEnumからGoのEnumに変換するConverterを並行処理を使って作成してみる
  • 並行処理を使って、複数のAPIレスポンスをまとめる処理を書いてみる
  • 並列処理のアルゴリズムを自分で作ってみる
  • プッシュ通知処理をゴルーチンを使って実装してみる

それぞれテーマが興味深く、かつ具体性もあったので取り掛かりやすかったのかなとも思います!

まずはみんなでワイワイランチ!

ランチの様子
お腹を満たしたら、それぞれもくもく作業!

2時間やったら中間進捗発表して、さらに2時間やって最終発表といった感じでメリハリをつけてもくもくすることができました。

ちょっとした発表時間があると、進めようという程よい緊張感もあり、集中して取り組めたと思います。 発表は実際にデモをしてみたり、ホワイトボードの前で解説したりとみんな進捗でてて最高でした!(写真撮り忘れた…)

勉強会も終わってパシャリ
ちょっとしたプチ開発合宿みたいな感じで楽しかったです!

最近はオフラインの場も増えてきたので、こういった機会をたまに設けると学びが加速しそうですね!

最後に

今回は「Go言語による並行処理」本の輪読会を有意義にするために行ったことを紹介させていただきました。

この輪読会は途中で日程が合わずに継続が危ぶまれる時もありましたが、なんとか完走することができました(実際開始から終了までのリードタイムとしては10ヶ月ほどかかっています…笑)。

難しい部分もありつつ、工夫もしながら完走できたのはすごい自信にも繋がったかなと思うので、また10月からも新しい本の輪読会に取り組む予定です!

みなさんも一人で読むと心が折れそうになる本は仲間を見つけてワイワイしながら取り組んでみるのはどうでしょうか?🙌

みんなで打ち上げ!
お疲れ様でした!!

DroidKaigi 2023 Day3 参加レポート

こんにちは!Androidエンジニアの関根です。

2023/09/14から3日間、DroidKaigi 2023が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、僭越ながらレポートをします。 少しでもAndroid開発の盛り上がりに貢献できたら嬉しく思います。

3日目は、これまでと趣向を変えてコミュニケーションを中心にしたコンテンツが行われました。 一覧にすると以下の通りです。

  • Codelabs / コードラボ -
  • Career Panel Discussion / キャリア・パネルディスカッション -
  • Career Advice Sessions / キャリア相談会 -
  • Meetups on different topics / 特定のトピックについてのミートアップ -

わたしは、コードラボとキャリア・パネルディスカッションに参加しましたので、2つのコンテンツを中心に感想を書かせていただきます。 補足ですが、バリスタの方が作るカフェラテの提供があり、リラックスして参加できました。

※Day1、Day2の様子と、バリスタの方のお店のWebサイトをAppendixに載せているので、そちらもお読みください。

キャリアパネルディスカッション

さまざまなバックボーンを持つパネリスト4名の、パネルディスカッションです。

2023.droidkaigi.jp

  • キャリアプランを立てるかどうか
  • キャリア形成をする上での行動や心構え
  • プライベートとキャリア

など、日常的には聞けないテーマが多く取り上げられました。slidoを使った質疑応答もあり、技術とは異なる観点で、コミュニティを支えるコンテンツだったと感じています。内容に共感しながら、業界を支えている方々の姿勢に触れ、背筋が伸びる思いがしました。

Codelab

Codelabsの中から選定された、3つのコースが用意されており、完了するとプレゼントがもらえるというコンテンツです*1

2023.droidkaigi.jp

わたしは「Jetpack Composeの基本」というコースを選んで取り組みました。

developer.android.com

じっくりとJetpackComposeに触れる機会を作れていなかったので、純粋に良い機会になりましたし、ランチタイムから同席になった方々と、お互いの作業内容の共有をしながら取り組めました。JetpackComposeやマルチモジュールの導入状況など、業務上での裏話を情報交換をできたので、貴重な時間になりました。

参加したコンテンツ以外にも、魅力的なコンテンツがありましたので、紹介しておきます。

Meetup

いくつかのテーブルに分かれ、特定のテーマを元に交流するコンテンツです。

2023.droidkaigi.jp

わたしはCodelabsに集中していたため、時間切れとなり、参加できませんでしたが、終始人が集まり交流が行われていました。オンライン主流の日常では、得難い機会であり、オフラインならではのコンテンツだと感じました。

キャリア相談会

パネルディスカッションのパネリストの方々に、少人数で、キャリア相談ができるコンテンツです。

2023.droidkaigi.jp

キャリアの悩みは、所属会社だけでは解決できないケースもあると思うので、コミュニティで相談できることは心強いと感じました。満席のアナウンスもあり、盛況だったようです。

オフィスツアー

スカラーシッププログラムの参加者向けのコンテンツで、DroidKaigiに協賛している企業のオフィスを見学するツアーです。

medium.com

コミュニティの一員として、学生を支援していることに、強く意義を感じます。オフィスツアーから戻った学生の方々を、拍手で迎えることもAndroidコミュニティの温かさを実感しました。

まとめ

スピーカーの皆様、スタッフの皆様、そして参加された皆様、3日間お疲れ様でした!

1日目、2日目では、技術的な見識を深く得られましたし、企業ブースではさまざまな業種の方々から、貴重な開発事例をお聞きできました。3日目には、Androidコミュニティの方々と、情報交換やもくもくコーディングできたので、有意義に過ごさせていただきました。

スポンサー一覧を見ると、モバイルアプリ以外への広がりを感じますし、キャリアについて相談したり、スカラーシップでの学生支援など、Androidコミュニティを支えるイベントに、より一層進化していることを実感しております。

DroidKaigiを支えてくれている皆様に、心から感謝です!また来年お会いしましょう!

Appendix

tech.connehito.com

tech.connehito.com

alphabetticafe.com

*1:完了しましたが、プレゼントをもらい忘れてしまいました・・・