CloudFront の TooManyInvalidationsInProgressException を CallerReference で抑止したい

前提

CloudFront のキャッシュを利用するシステムで、ユーザーの操作をトリガーに、条件に合致するオブジェクトパスに invalidation リクエストする仕様。

困ったこと

com.amazonaws.services.cloudfront.model.TooManyInvalidationsInProgressException: Processing your request will cause you to exceed the maximum number of in-progress wildcard invalidations.

進行中の invalidation プロセスにおいて、ワイルドカード (*) を含むオブジェクトパスの指定は最大15個までで、その上限を超えるとエラーが返る模様。

同時無効化リクエストの最大制限 | ファイルの無効化 - Amazon CloudFront

たまに CloudFront 側で進行中の invalidation が滞留することがある。invalidation のオブジェクトパスにワイルドカード (*) を含む場合に、表題のエラーが発生することがある。特に、同一ユーザーが連続して操作した際に、同様の invalidation プロセスが複数生成されてしまうのが痛い...。

対策

今回は、invalidation の CallerReference を利用してエラーの発生を抑えてみる。

単位時間あたりの同じオブジェクトパスに対する invalidation において、CallerReference に同じ値を指定することによって短時間で同じオブジェクトパスに対する invalidation が実行できなくなる。正確には、invalidation リクエストは投げるが、CloudFront 側で invalidation プロセスが生成されない。

CallerReference にどういう値を指定するか

docs.aws.amazon.com

このドキュメントの CallerReference に書かれているように、単純にタイムスタンプを指定するだけだと別々のユーザーが同時に操作した場合に invalidation がコンフリクトする可能性がある。というわけで、ユーザーのリクエスト情報を CallerReference に含める。

今回は、単位時間を1分間として、リクエストパラメータのオブジェクトパスを並び替えて結合したものを利用する。最終的には yyyyMMddHHmm_{param1}-{param2}-... のような文字列を CallerReference に指定する。

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

private String callerReference(List<String> params) {
    var str =  String.format("%s_%s",
            LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")),
            String.join("-", params.stream().sorted(Comparator.naturalOrder()).collect(Collectors.toList()))
    );
    return str.substring(0, Math.min(str.length(), 128)); // max 128 characters
}

単位時間を3分間にするならこんな感じ。

var currentDateTime = LocalDateTime.now();
var rounded = currentDateTime.withMinute(currentDateTime.getMinute() - (currentDateTime.getMinute() % 3)).format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));

これで、短時間で同様の invalidation は実行されなくなるので、表題のエラー抑止にも効果がありそう。

補足)

com.amazonaws.services.cloudfront.model.InvalidArgumentException: The parameter CallerReference is too big.

CallerReference に指定できるのは最大128文字までの模様...。仕方ないので128文字で切り出している。

問いのデザイン: 創造的対話のファシリテーション

『問いのデザイン』を読んでみました。

www.amazon.co.jp

ワークショップとかをやるわけではないけれど、スクラムイベントをファシリテーションしたり、チームメンバーと 1on1 したりするので、そのあたりに活かせるものがないかなと期待して読んでみました。

「問いは問われた側の思考と感情を刺激する」と書かれている通り、こちらからどのように問いかけるかが重要なんだと思います。真正面から質問をぶつけるだけでなく、視点を変えたり、より本質を捉えるような「ひと捻り」を加える必要がありそうです。どちらかというとこういうコミュニケーションが苦手...。

大きく分けると課題とプロセスの話があるけど、個人的には、課題をどう設定するかがキモな気がする。そのあたりのリフレーミングとか課題のよしあしをどう判断するかみたいなところは参考になりました。


以下、メモから抜粋。

問いのデザイン
  • 問いは問われた側の思考と感情を刺激する
  • 問いは集団のコミュニケーションを誘発する
    • 創造的対話
  • 課題のデザイン + プロセスのデザイン
    • 問題の本質を捉える, 解くべき課題を定める
    • 問いを投げかけて創造的対話を促進する
課題設定の罠
  • 自分本位
  • 自己目的化 (手段が目的になっているパターン)
  • ネガティブ, 他責
  • 優等生 (1つの側面しか見れていないパターン)
    • あるべき以外の状態に着目する
  • 壮大
    • 当事者が自分事にできるサイズまで落とし込む
目標の精緻化
  • 期間 (短期, 中期, 長期)
  • 優先度
  • 性質 (成果, プロセス, ビジョン)
    • 到達点の成果までにどのようなプロセスを辿るべきか
目標の再設定 (リフレーミング)
  • 利他的に考える
    • 自分本位にならないように焦点を他者に向ける
  • 大義を問い直す
    • 自己目的化しないように大義を問い直してビジョンや成果目標に反映する
  • 前向きに捉える
    • ネガティブ/他責にならないように相互に建設的な目標に変える
  • 規範外
    • 天邪鬼思考/逆説的に考える
    • ビジョンや成果目標そのままでプロセス目標にひねりを加える
  • 小さく分割する
    • 壮大な目標にならないように
    • 優先度や構成要素で分割する
  • 名詞を動詞に言い換える
  • 言葉を定義する
    • 言語化して曖昧さをなくす
    • 共通の目標を決める (メタ目標)
  • 主体を変える
  • 時間尺度を変える
    • 成果目標は変えずにビジョンを極端に未来に飛ばす
    • 未来志向の目標に対して過去に視点を向けるプロセス目標を設定する
  • 第三の道を探る
よい課題の判断基準
  • 効果性
    • 問題の本質を捉えているか
  • 社会的意義
    • どれくらい社会に付加価値をもたらすか
  • 内発的動機
    • 自分事になっているか
プロセスデザイン (プログラムを計画する)
  • 参加者への最初の問いかけが重要
    • 意図が見えない曖昧な問いかけは逆効果になる
    • 焦点の定まった問いを投げかける
  • 共同体, 空間, 人工物, 活動
    • どんな参加者が, どこで, 何を使って, どんな順序で
  • 導入 (アイスブレイク) > 知る (情報のインプット) > 創る > まとめ (ふりかえり/経験の意味付け)
  • 課題解決に必要な経験を促す
    • 社会や組織などのマクロな視座を重視するか
    • 個人の視座を重視するか
  • 社会/組織+過去 > 歴史
  • 社会/組織+未来 > ビジョン
  • 個人+過去 > 経験
  • 個人+未来 > 妄想
  • 問いの制約を設定する
    • 価値基準を示す形容詞
      • よい xxx とは > 快適な xxx とは
    • ポジティブ, ネガティブ
    • 時期や期間を指定する
    • 想定外の制約を付ける
    • 問いに対するアウトプットの形式に制約を付ける
  • 意味レベルの問い, 仕様レベルの問い
  • 具体的な経験 > 抽象的な問い > 具体的な仕様に落とし込む
  • 点数, グラフ
    • 点数の意味付け, 点数が高い状態を想像させる
  • ものさし作り
    • 価値の評価基準のすり合わせ
  • 架空設定
  • そもそも
  • 喩える
ファシリテーターのコアスキル
  • 説明力
  • 観察力
  • 即興力
  • 情報編集力
    • 共通点/相違点を探る, 情報を構造化する, 視点の不足を探る
  • フレーミング
  • 場のホールド力 (コントロール)
4タイプの問いかけ
  • シンプルクエスチョン
  • ティーチングクエスチョン
    • 教育的な意図を持って介入するパターン
    • 不足している視点への気付きを促す
  • コーチングクエスチョン
    • 意欲, 思考, 価値観 を引き出す
  • フィロソフィカルクエスチョン
    • そもそもを問う
ALACT モデル

xargs と curl で並列にリクエストを投げる

備忘録。

パフォーマンステストで Vegeta を使うことがたまにある。

Vegeta attack - kntmr-blog

ただ、単純にたくさんリクエストを投げるだけなら xargs + curl が使えそう。あと、Vegeta だとレスポンスヘッダとかが見れないっぽいが、この方法ならなんとか見れそう。

以下は50並列でリクエストを投げるパターン。

$ seq 50 | xargs -n 1 -P 50 sh -c 'curl -I -XGET https://...'

現場からは以上です。

.a.i.SimpleAsyncUncaughtExceptionHandler : Unexpected exception occurred invoking async method ...

備忘録。

@Async で非同期に処理するメソッドの中で例外が発生した場合、表題のようなエラーが出力される。

2022-01-26 22:44:58.608 ERROR 3401 --- [         task-1] .a.i.SimpleAsyncUncaughtExceptionHandler : Unexpected exception occurred invoking async method: public void com.example.SampleService.execute()

デフォルトでは、SimpleAsyncUncaughtExceptionHandler クラスでハンドリングされるため。以下抜粋。

public void handleUncaughtException(Throwable ex, Method method, Object... params) {
    if (logger.isErrorEnabled()) {
        logger.error("Unexpected exception occurred invoking async method: " + method, ex);
    }
}

この例外をハンドリングする場合は、AsyncUncaughtExceptionHandler インタフェースを実装したクラスを用意する。

@Component
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
        log.warn("Unexpected asynchronous exception!!");
    }
}
@Configuration
public class Config extends AsyncConfigurerSupport {
    @Autowired
    AsyncExceptionHandler asyncExceptionHandler;
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return asyncExceptionHandler;
    }
}
2022-01-26 23:12:00.616  WARN 3846 --- [         task-1] c.e.AsyncExceptionHandler  : Unexpected asynchronous exception!!

現場からは以上です。

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"

現場からは以上です。