2021年のふりかえりと2022年に向けて

謹賀新年。年末年始は帰省先で過ごしています。2021年はあっという間に終わってしまった感。

2020年のふりかえりと2021年に向けて - kntmr-blog

仕事に関しては、たぶんこれまでとは違った価値観を身に付ける必要がある気がする。とはいえ、すぐに身に付くようなものではないと思うので、小さなことからコツコツと。その中で自分ができること/やれることをしっかりやって自分の価値を出していければいいかなって。

転職1年のふりかえり から抜粋。

  • スクラムイベントを効果的にやりたい
    • 逆にできないものは無理してやらない判断をしたい
    • 手段が目的にならないように
  • 1on1 を効果的にやりたい
    • 雑談力が欲しい
  • メトリクス監視を習慣にしたい
  • SRE 力を身に付けたい
  • 仕事以外のインプットを増やしたい
  • 副業したい

仕事に関して、2022年にやることはだいたいこんな感じになりそう。

特に、自分の役割的にスクラムやプロジェクトマネジメントのスキル&経験を積むのが最優先かもしれない。前に PMBOKガイド 第7版 を買ったけど、積読しちゃってるので読もうっと。

あと、今年はフルフレックス&フルリモートが基本になりそうで、それ前提でオンボーディングやチームビルディングを考えないと。チームのコミュニケーションが増えるようにモブプロとかペアプロを活用していきたいところ。

リモート中心になるなら仕事の環境をもう少し整えたい。(今は WiFi の都合でリビングで仕事してるので、メッシュ WiFi とかにして別の部屋に移動したい)

今後、SRE のスキルを身に付けて Embedded SRE の役割を担えるようになりたい。というか、結局、AWS SAA の試験を受けずに2021年が終わってしまった...。あと、昨年後半は少し電車通勤を再開してその時間を利用してスタディサプリ ENGLISH を進めてたけど、今年はまたリモート中心になりそうなので進捗なくなりそう...。

以前のふりかえりでも書いたけど、最近、日頃の疲れからか、本を読んだりコードを書いたり情報収集する時間が減っている。こういうのは無理矢理にでも習慣にした方がよいと思うのでなんとかしないと。とりあえず何冊か積読している本を消化する。

2020年頃までは技術関連の情報を中心にキャッチアップしていた感じだったけど、昨年頃からは自分の役割的にチームマネジメント関連の情報収集が増えてきている気がする。

AWS CLI で CloudWatch メトリクスを取得する

調べる機会があったのでメモ。CloudWatch メトリクスは GetMetricData で取得する。

aws.amazon.com

今回は、10分間のメトリクスを取得する例。日時はオフセット付きでも指定可。(2021-12-24T09:00:00+09:00)

$ aws cloudwatch get-metric-data \
  --metric-data-queries '{JSON}' \
  --start-time 2021-12-24T00:00:00Z \
  --end-time 2021-12-24T00:10:00Z

--metric-data-queries に指定する JSON はこんな感じ。今回は、2つの RDS (RoleREADER/WRITER で分かれている) の CPUUtilization を取得し、Metric Math で平均 (AVG) を算出する。

それぞれの CPUUtilization は 60sec 間隔 ("Period": 60) で平均 ("Stat": "Average") を取っている。途中計算に利用するメトリクスは、"ReturnData": false にすることで最終的なレスポンスには表示されなくなる。

docs.aws.amazon.com

[
  {
    "Id": "m0",
    "Expression": "AVG([m1, m2])",
    "Label": "AverageCPUUtilization"
  },
  {
    "Id": "m1",
    "MetricStat": {
      "Metric": {
        "Namespace": "AWS/RDS",
        "MetricName": "CPUUtilization",
        "Dimensions": [
          {
            "Name": "DBClusterIdentifier",
            "Value": "production-cluster-a"
          },
          {
            "Name": "Role",
            "Value": "WRITER"
          }
        ]
      },
      "Period": 60,
      "Stat": "Average"
    },
    "ReturnData": false
  },
  {
    "Id": "m2",
    "MetricStat": {
      "Metric": {
        "Namespace": "AWS/RDS",
        "MetricName": "CPUUtilization",
        "Dimensions": [
          {
            "Name": "DBClusterIdentifier",
            "Value": "production-cluster-a"
          },
          {
            "Name": "Role",
            "Value": "READER"
          }
        ]
      },
      "Period": 60,
      "Stat": "Average"
    },
    "ReturnData": false
  }
]

レスポンスはこんな感じ。これを jq とかで Values の値を取ってきてごにょごにょすればいろいろできそう。

{
    "MetricDataResults": [
        {
            "Id": "m0",
            "Label": "AverageCPUUtilization",
            "Timestamps": [
                "2021-12-24T00:00:00+00:00"
            ],
            "Values": [
                4.780129741529112
            ],
            "StatusCode": "Complete"
        }
    ],
    "Messages": []
}

その他

date コマンドで現在日時を取得する。

$ date -u +"%Y-%m-%dT%H:%M:%SZ"

現在日時から10分前の日時を取得する。

$ date -u +"%Y-%m-%dT%H:%M:%SZ" -d "10 min ago"

現場からは以上です。

CloudFront のキャッシュを Lambda から invalidation する

調べる機会があったのでメモ。

前提

Lambda 関数はこんな感じで作成。IAM ロールは別途用意。

サンプルコード

今回は、invalidation するオブジェクトパスをパラメータで指定できるようにする。パラメータ未指定の場合はデフォルトのパス (/*) で invalidation を実行する。

CallerReference は冪等性を担保する仕組みのようで呼び出しごとに一意の値を渡せばよさそう。(今回は UUID とする)

import boto3
import uuid

client = boto3.client('cloudfront')

def lambda_handler(event, context):
    distribution_id = '{distribution_id}'
    items = event['items'] if 'items' in event else ['/*']
    resp = client.create_invalidation(
        DistributionId = distribution_id,
        InvalidationBatch = {
            'Paths': {
                'Quantity': len(items),
                'Items': items
            },
            'CallerReference': str(uuid.uuid4())
        }
    )
    print('create_invalidation success:')
    print(resp)

オブジェクトパスを指定して invalidation を実行する

オブジェクトパスをパラメータに指定して Lambda を テスト実行 する。

{
    "items": [
        "/path/to/foo",
        "/path/to/bar"
    ]
}

定期的に invalidation を実行する

設定 > トリガー で EventBridge (CloudWatch Events) のトリガーを設定する。ルールタイプをスケジュール式にして rate(10 minutes) とすると10分間隔で Lambda が起動する。(cron 形式でも指定可)

(補足1)

IAM ロールにはこんな感じの IAM ポリシーをアタッチする。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:CreateInvalidation"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

(補足2)

AWS CLI で CloudFront のキャッシュを invalidation する。

$ aws cloudfront create-invalidation --distribution-id {distribution_id} --paths "/path/to/foo" "/path/to/bar"

現場からは以上です。

イミュータブルでゆこう に参加した #現場から学ぶモデル駆動設計

イミュータブルでゆこう に参加しました。簡単に所感をまとめます。

modeling-how-to-learn.connpass.com

所感

イベントとリソースを区別したりイベント (コト) に注目するというのは、頭では分かっていても実際に設計するときにちゃんと実践できるだろうか...。どうしてもヒトやモノというか、UI やテーブル設計に意識が向きがちなので、このあたりを転換できるようにしたいです。

最近、イベントデータを集約する処理でレイテンシが悪化して問題になる事例があった (というか自分が作り込んだ...) ので、スナップショットを取る方法は試してみたいところ。

以下、メモから抜粋。

イミュータブルデータモデルの極意

www.slideshare.net

  • 場合分けを形式知になっていない
  • data, inform, information
    • データは事実の集合
    • データだけでは意味をなさない
    • 選択/加工して知識を取り出す > information
  • data > event, resource
  • event (コト)
    • 日時属性を持つ
    • 時系列の並びが重要 (非対称性)
    • 例) 会員
  • resource (モノ)
    • 日時属性は持たない
    • ライフサイクルに伴って属性が変化する
    • 属性が変化しても identity は変わらない
    • 例) 会員登録
  • イベントによってリソースが生成/更新/削除される
  • イベントを永続化するかどうかは業務設計による
  • イベント-リソース
    • イベントの7W3Hがリソースと関連を持つ
  • リソース-リソース
    • 親子関係/依存関係
    • ライフサイクルが同じかどうか
    • 依存/非依存は業務の制約次第
  • イベント-イベント
    • 時系列, 並びが変わると関係が変わる
    • イベントデータを更新しないように設計する
    • 例) 注文-請求
      • 注文イベント生成前に請求イベントは存在しない
      • 注文イベントに請求の属性は持たせない
  • サブタイプ
    • 共通の性質をスーパータイプに持たせる
    • 差分の性質をサブタイプに持たせる
    • リソースのサブタイプは区分で分類する (区分をスーパータイプに持たせる)
  • イベントが親子構造を持つケース
    • 親子のイベントをまとめてイベントとする
    • 子のイベントに後続のイベントが関連付くこともある
    • 子のイベントが日時属性を持っていなくてもイベントとして扱う
  • 一連のイベントのステータス
    • ロングタームイベント
  • リソース同士を関連付けるイベント
    • リソース間の関連と、それに関するイベントは区別する
    • 例) 所属, 配属イベント
  • どのイベントを記録として残すか
    • リソースの生成/変更/削除 > 変更履歴
    • イベント発生から次のイベントが生成される > 業務フロー
    • 非依存のリソースの関連付け/解消 > 業務イベント, 操作履歴
  • お金を生むもの, 記録がないとお金を失うリスクがあるもの
  • イベントは変更してはならない (事実が失われてしまうので)

あとで読む

ドメイン駆動設計とイミュータブルデータモデルの素敵な関係

  • 必ずイミュータブルにする
  • 閉じた操作
    • 引数の型と戻り値の型はそのクラスの型に閉じる
  • withメソッド (setterの代わり)
    • 不変なオブジェクトを元に別インスタンスを作成して返す
    • 部分的な変更
  • イベントリポジトリ/集約ファクトリ
    • 事実の記録と集約の構築は非対称
    • 集約は永続化しない
    • 集約を変更する際は先に事実を記録してから集約インスタンスを生成する
    • 同じ事実からは同じ結果/集約が返る (一貫性)

ドメインイベントの観点から再考するソフトウェア設計

  • ドメインイベントは過去に起きたドメイン上の出来事
  • Event Sourcing に限らない
  • ドメインイベント = 事実
  • 事実 = 事態 + 成立
  • コトに注目する
    • コトはヒト/モノ同士を関連付ける
    • 時間軸に沿って前後関係を持つ
  • ヒト/モノから分析しない
  • コトを手掛かりに分析する
  • Event Sourcing
    • イベントからステートを作り出す
    • applyEvent
  • イベントは追記のみ (更新はしない)
    • ロックがないのでスケールしやすい
  • 非同期連携しやすい
  • 大量イベントをリプレイするとレイテンシが悪化する
    • 一定のタイミングでスナップショットを取る
  • Event Storming
    • コトからモノを探索する
    • イベントはコマンドが受理されたときに生成される
  • イベントのデータストアは RDB である必要はない
    • NoSQL とか...
  • Akka-ES モデル
    • 集約アクター内のオブジェクトグラフがデータソースとなる
    • シャーディング

その他

  • デフォルトは不変
  • 可変は局所的にする
  • イミュータブルはテストしやすい
  • ビジネスロジックでは戻り値を返さないことはない
  • void を使うのは外部の書き込みくらい
  • NOT NULL, 追記のみ (UPDATEしない)

ふりかえり

転職して1年。転職3ヶ月以降の期間をふりかえってみたり。

kntmr.hatenablog.com

やったこと

前半は主にマーケ寄りの施策を担当し、いくつかの新機能をリリース。

夏頃にエンジニアチームの体制が再編成されて、toB 寄りの開発チームのリーダーを担当。とある開発プロジェクトで PM として仕様を決めたり設計/開発をしつつ、チームメンバーのタスクをサポートしている。チーム参画直後に担当したプロジェクトでは、リーダーとしての立ち回りに悩みつつ、仕様検討や設計/開発をやっていたのでそこそこ心身疲弊した...。

あと、問い合わせ対応や打ち合わせ、チームメンバーのタスクのレビューなどが増えて、集中して作業できる時間があまり取れないのが悩みどころ。特に問い合わせ対応に関しては、担当する事業領域が変わったことでドメイン知識が足りず苦労している。

一応、チームはスクラムの体制になっており、自分はスクラムマスターのような役割を担わないといけないかもしれないが、スクラムイベントがちゃんと回せていない気がする...。もともと稼働していたチームに自分が合流した形になるので、最初は「郷に入っては郷に従え」みたいなスタンスでやってきたが、一度立ち止まって改めてチームビルディングしたいところ。バックログにも古くて優先度がよく分からないタスクが溜まってるし、ちゃんとリファインメントしたい。

これからどうする

  • スクラムイベントを効果的にやりたい
    • 逆にできないものは無理してやらない判断をしたい
    • 手段が目的にならないように
  • 1on1 を効果的にやりたい
    • 雑談力が欲しい
  • メトリクス監視を習慣にしたい
  • SRE 力を身に付けたい
  • 仕事以外のインプットを増やしたい
  • 副業したい

最近、日頃の疲れからか、本を読んだりコードを書いたり情報収集する時間が減っている気がする。こういうのは無理矢理にでも習慣にした方がよいと思うのでそろそろ活動再開しないと...。現場からは以上です。

モデルベースで要件定義をやってみた に参加した #現場で役立つモデル駆動設計

モデルベースで要件定義をやってみた に参加しました。簡単に所感をまとめます。

modeling-how-to-learn.connpass.com

所感

途中で言ってたけど、ステークホルダが RDRA のモデルでコミュニケーションできるようになるのは理想の世界かもしれない。で、そのためには非エンジニアでも読みやすいように RDRA のモデルを工夫して書く必要があるようです。Figma + Instance Finder はよさそう。ただ、個人の思考ツールとして使うならそこまで作り込む必要はないんだろうけど。

業務の本質は RDRA で表現しにくいっていうのは興味深い視点。あと、RDRA は広さと深さを自由自在に行き来できるっていうのがいいポイント。

以下、メモから抜粋。

RDRAはどう形作られたか?

www.slideshare.net

  • 要件を決められない
  • 要件定義の精度が悪い
    • 暫定仕様が増え続ける
    • システムに近いところの機能を定義している
  • 精度を高める
    • 整合性は合っているか, 網羅しているか
  • 見積もり根拠
  • 要件を容易に変更したい
  • 全体の共通認識を得たい
  • What と Why を表現する
    • How は表現しない
  • ビジネうルールを表現する
    • 業務フローの単位を決める
  • 精度の高い要件 = 仕様の足場
  • ビジネスルールは条件 → 状態モデルで表現する
  • ビジネスユースケースを洗い出す
    • BUCがわかると全体のボリュームがわかり計画が立てられる

RDRA導入後の要件定義の変化

www.slideshare.net

  • 業務設計チーム
    • 企画と開発の橋渡し
    • プロダクトマネージャー?
  • 改修案件のQCDを上げたい
  • RDRAモデルをマスタにする
    • 改修案件でマスタから引用する
    • マスタにフィードバックする
  • BUCをプロダクトの業務として定義する
    • プロジェクトごとの業務フローの粒度をズレを防ぐ
  • 業務フロー, 状態モデル
    • 日本語による曖昧さを回避する
  • 非エンジニアでもRDRAモデルが読みやすくなるように工夫する
    • 記法の統一, 可読性の改善, 補助資料, ...
  • ステークホルダとの合意形成ツール

新規サービス開発で RDRA を使っている話

  • spreadsheet
  • アクター, 外部システム, 情報の洗い出し
  • ユースケース洗い出し
    • アクティビティ, UC の抽出粒度が難しい
    • 業務の理解度によって粒度が変わりそう
    • ヒアリング
  • 業務とアクターの分析
    • 誰がどのような業務をやっているか
    • 抜け漏れが見つけられる
  • 情報/状態の構造化
    • 情報の関連を引く
    • バリエーションのリストアップ
  • 要求の洗い出し
    • 業務要求/非機能要求を整理する
  • IT要求の解像度を上げる
  • ToBe の全体イメージを早めに付ける
    • 必要に応じて AsIs を詳細化する
  • 情報モデルの検討
    • オブジェクトモデルがあるとよい
  • アクター, BUC 洗い出し
    • ユースケースを細かくしていくと状態やバリエーションが見つかる
    • RDRAモデルで別の形で表現できたり
  • 状態モデルの検討
  • ToBe モデルを書き進めると自然と BUC が精査されていく
  • 粒度が粗いところは理解が不足しているかもしれない
    • 現状分析が必要

RDRAと業務と私

  • ヒアリング/観察
  • 業務を理解することで要件定義ができるように
  • チームの業務理解度を上げる
  • 書かないと読めるようにならない
  • ユーザーがシステムを触っている目線で書く
    • エンジニア視点だと細かくなりがち
  • ユーザーと一緒にRDRAを書く (モブプロ)
  • Enterprise Architect
  • Figma + Instance Finder
  • RDRA + JIG
  • 業務の本質はRDRAで表現しにくい

RDRA2.0を1年半つかって実感した効果

  • RDRAでそれぞれの関係性が整理できる
  • データ系モデルが作りやすい
  • 見積もりの精度をあげるときは仕様化/設計する
  • 設計/実装にスムーズに入れる
  • 段階的詳細化
    • 抽象 → 具体
  • 広さと深さを自由自在に行き来できる
    • 広さ (要件のリスト)
    • 深さ (要件の詳細化)
  • ユーザーストーリーマッピングは広さを担保するのが大変
    • 1つのマップを深堀りするため

ドメイン駆動設計にRDRA2.0を活用する

  • ユースケースをちゃんと洗い出せているか → RDRA
  • 視点を増やす
  • ユースケース ← 業務フロー ← ビジネスユースケース (業務バリエーション)
  • 関係で考える
  • 事業活動のモデル (ビジネスコンテキスト)
    • 業務のバリエーション
    • ビジネスルールを漏れなく見つける
  • それぞれは完全に洗い出せていなくても、それぞれの関係から考えることで抜け漏れを見つけられる
  • ビジネスルールとドメインモデルを固める
  • 共通認識のツールとしてRDRAのモデルを仕上げる

その他

  • クロスリファレンスできるツールが欲しくなる
  • spreadsheet はみんなでやるときに早くていい
  • ビジネスユースケースの粒度を合わせたい
  • 他のモデルを書いていくと粒度が合っていく
    • 理解が進んでバランスが取れていく
  • ピクト図解
  • 非機能要件 > 要求モデル図に書く
  • モジュラーモノリスで作る
    • コンテキストが明確になってから切り出した方がよさそう
  • BUCは早く出す
    • アクティビティを洗い出す
    • グルーピングして名前を付けてBUCを出した方がよさそう
  • RDRA
    • 個人の思考ツールとして使う
      • 実装に移行して以降は JIG で見る
    • マスタとして使う
  • revise しやすく保つためには軽くしておくとよさそう

AWS SDK for Java で S3 のファイルをまとめてダウンロードしたい

S3 のフォルダにあるファイルを zip でまとめてダウンロードしたい。今回は AWS SDK for JavaAmazonS3#getObject を非同期で呼び出して ZipOutputStream で書き出してみる。

サンプルコードはこちら。

github.com

前提

備忘録

起動クラスに@EnableAsync アノテーションを付けて非同期処理を有効にする。

@EnableAsync
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
    // ...(略)

任意の prefix に一致するオブジェクトリストを取得する。サブフォルダは除く。

public List<String> listObjectKeys(String bucketName, String prefix) {
    return amazonS3.listObjectsV2(bucketName, prefix)
            .getObjectSummaries().stream()
            .filter(object -> !object.getKey().endsWith("/")) // サブフォルダは除く
            .map(object -> object.getKey())
            .collect(Collectors.toList());
}

AmazonS3#getObject を非同期で呼び出す。戻り値には CompletableFuture<T> を返す。ドキュメントを読む限りでは AmazonS3 はスレッドセーフっぽい。(AmazonS3ClientBuilder の方はスレッドアンセーフ)

サービスクライアントの作成 - AWS SDK for Java

@Async
public CompletableFuture<S3Content> fetchAsync(String bucketName, String objectKey) {
    S3Object object = amazonS3.getObject(new GetObjectRequest(bucketName, objectKey));
    return CompletableFuture.completedFuture(new S3Content(object));
}

オブジェクトリストの objectKey ごとに @Async な非同期処理を呼び出して処理完了を待つ。

List<S3Content> s3Contents = new ArrayList<>();
List<CompletableFuture<S3Content>> processes = new ArrayList<>();
for (String objectKey : objectKeys) {
    CompletableFuture<S3Content> process =
            service.fetchAsync(bucketName, objectKey).whenCompleteAsync((res, e) -> s3Contents.add(res));
    processes.add(process);
}
CompletableFuture.allOf(processes.toArray(new CompletableFuture[objectKeys.size()])).join();

byte 配列のデータを ZipOutputStream で書き出す。今回は ServletResponse#getOutputStream をラップして呼び出し元に返すようにする。

String basename = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));

response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + basename + ".zip");
response.setStatus(HttpServletResponse.SC_OK);

try (ZipOutputStream out = new ZipOutputStream(response.getOutputStream())) {
    for (S3Content content : s3Contents) {
        out.putNextEntry(new ZipEntry(Paths.get(basename, content.filename()).toString()));
        try (InputStream in = new ByteArrayInputStream(content.data())) {
            byte[] buf = new byte[4096];
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
        }
    }
}