HackToTech

Hack To Technology

AWS CDKでdiffが出るのにdeployでnochangesになる

最近CDK使ってて出会した
端的に言えばCFnが悪いやつ(下はGithubのIssue)

github.com

ただまあ公式にも書いてあるこれをどうにかしてデプロイしたい

CreationPolicy、DeletionPolicy または UpdatePolicy 属性単独では更新できません。
更新できるのは、リソースを追加、変更、または削除する変更を含める場合だけです。
たとえば、リソースのメタデータ属性を追加または変更することはできます。

docs.aws.amazon.com

しかしながら一部変更を適用するのに、
いちいちリソースを変更まではしたくないなあと思って適当に回避策を思いついて試したらうまく動いたので残しとく

まずは適当にCDKのアプリケーションを作る(今回はTypeScriptだけど他の言語でも出来るはず)

cdk init app --language typescript
# 作ると勝手にディレクトリ移動されるので今回はS3を使う
npm install @aws-cdk/aws-s3

作ったらまずは適当にBucketを作る

import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';

export class CdkTestStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    new s3.Bucket(this, 'TestBucket', {
      bucketName: 'ex-test-bucket-000000'
    });
  }
}

デプロイする

npm run cdk -- deploy

で、作ったあとにやっぱRemovalPolicyにDESTROYをつけたくなったとする

new s3.Bucket(this, 'TestBucket', {
  bucketName: 'ex-test-bucket-000000',
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

diffが出る(元々デフォルトでRetainがついてるので)

npm run cdk -- diff

> cdk-test@0.1.0 cdk
> cdk "diff"

Stack CdkTestStack
Resources
[~] AWS::S3::Bucket TestBucket TestBucket560B80BC 
 ├─ [~] DeletionPolicy
 │   ├─ [-] Retain
 │   └─ [+] Delete
 └─ [~] UpdateReplacePolicy
     ├─ [-] Retain
     └─ [+] Delete

デプロイするとNo changes!!!!😇

npm run cdk -- deploy

> cdk-test@0.1.0 cdk
> cdk "deploy"

CdkTestStack: deploying...
CdkTestStack: creating CloudFormation changeset...

 ✅  CdkTestStack (no changes)

というわけで↓を新たに入れておく

new cdk.CfnOutput(this, 'UpdateTime', {
  description: 'UpdateTime',
  value: new Date().toLocaleString(),
})

diffが増える

npm run cdk -- diff

> cdk-test@0.1.0 cdk
> cdk "diff"

Stack CdkTestStack
Resources
[~] AWS::S3::Bucket TestBucket TestBucket560B80BC 
 ├─ [~] DeletionPolicy
 │   ├─ [-] Retain
 │   └─ [+] Delete
 └─ [~] UpdateReplacePolicy
     ├─ [-] Retain
     └─ [+] Delete

Outputs
[+] Output UpdateTime UpdateTime: {"Description":"UpdateTime","Value":"2021/7/22 16:51:32"}

デプロイをする
今回はリソースを追加したので当たり前だが成功する

npm run cdk -- deploy

> cdk-test@0.1.0 cdk
> cdk "deploy"

CdkTestStack: deploying...
CdkTestStack: creating CloudFormation changeset...

 ✅  CdkTestStack

Outputs:
CdkTestStack.UpdateTime = 2021/7/22 16:52:02

今度はRemovalPolicyをRETAINに戻したくなったとする

new s3.Bucket(this, 'TestBucket', {
  bucketName: 'ex-test-bucket-000000',
  // removalPolicy: cdk.RemovalPolicy.DESTROY, これを削除する
});

diffを見る
(ぱっと見、上で失敗したときのパターンに見える)

npm run cdk -- diff

> cdk-test@0.1.0 cdk
> cdk "diff"

Stack CdkTestStack
Resources
[~] AWS::S3::Bucket TestBucket TestBucket560B80BC 
 ├─ [~] DeletionPolicy
 │   ├─ [-] Delete
 │   └─ [+] Retain
 └─ [~] UpdateReplacePolicy
     ├─ [-] Delete
     └─ [+] Retain

これをdeployするとOutputsのUpdateTimeが変わる為、CDK(CFn)はちゃんとデプロイしてくれる

npm run cdk -- deploy

> cdk-test@0.1.0 cdk
> cdk "deploy"

CdkTestStack: deploying...
CdkTestStack: creating CloudFormation changeset...


 ✅  CdkTestStack

Outputs:
CdkTestStack.UpdateTime = 2021/7/22 16:56:21

再度diffを叩くと何も変更なしになる
(リソースへの変更は適用された為)

npm run cdk -- diff

> cdk-test@0.1.0 cdk
> cdk "diff"

Stack CdkTestStack
There were no differences

この状態でdeployするとUpdateTimeに対しての変更がかかる

npm run cdk -- deploy

> cdk-test@0.1.0 cdk
> cdk "deploy"

CdkTestStack: deploying...
CdkTestStack: creating CloudFormation changeset...


 ✅  CdkTestStack

Outputs:
CdkTestStack.UpdateTime = 2021/7/22 16:58:23

とまあこんな感じでOutputsにUpdateTimeとかを設定しておけば、
管理したいリソースに対して変更を加えずともOutputsに変更を加えて半強制的にデプロイをすることが出来て便利という小技

GC本を読んで学ぶ2

  • 全てのガベージコレクションは以下のいずれかに基づいている(ガベージコレクタ毎にさまざまな方法で領域ごとにこれらのアプローチを組み合わせているらしい)
    • マークスイープGC
    • コピーGC
    • マークコンパクトGC
    • 参照カウントGC

大体wikiに載ってる

  • ストップザ・ワールド的なアプローチ例
    • コレクタスレッド1
      • ガベージコレクションを実行するスレッド
    • ミューテータスレッドN
      • アプリケーションコードを実行するスレッド(コレクタスレッド実行時は停止)

これの利点はヒープのスナップショットがとれる + ヒープ中のオブジェクトのトポロジが変化しないこと

  • 自動メモリ管理を行うのに必要な機能
    • 新しいオブジェクトの領域の割当
    • 生きているオブジェクトの特定
    • 死んだオブジェクトの使用している領域の回収

完全に判別するのは厳しいので参照を追って参照が存在しないオブジェクトは死んでいる判定にすることによって安全に回収する

  • マークスイープGC
    • まずルート集合(レジスタ、スレッドのスタック、グローバル変数)からオブジェクトのグラフを走査して到達可能なオブジェクトにマークをつける(所謂tracing)
    • スイープフェーズでヒープ中の全てのオブジェクトにマークがついているか調べてなければゴミとして回収する(わかりやすい)
    • 間接的GCアルゴリズム
    • ゴミを探すというよりは生きているものを探して他は全てゴミとして扱う

Mark-and-Sweep: Garbage Collection Algorithm)

わかりやすい
コレクタがシングルスレッドなら探索リストをDFSで実装できる(これによってハードウェアキャッシュがあたることを期待できる)

三色抽象化を利用してオブジェクトの状態を表現できるらしい
黒(処理済み)、白(未処理(永遠に未処理の可能性もある))、灰(処理途中もしくは再処理が必要)
並行GCのアルゴリズムで特に有用らしい

  • 時間的局所性

    python total = 0 for i in range(1, 11): total += i

    とした場合にtotalに対して複数回のアクセスが発生するので、その値をキャッシュするのが有効(アドレスをイメージしたほうがわかりやすそう)

  • 空間的局所性

    python l = [1,2,3,4,5] for i in l: print(i)

    この場合lに対して複数回のアクセスが発生するので、その値付近をキャッシュするのが有効(これもメモリが連続して確保されている際にアクセスすることを考えたほうがわかりやすいか、CPUのプリフェッチとかもこれ系)

マークビットの領域は通常オブジェクトのヘッダワードの中に用意する
いまいちオブジェクトのヘッダワードのイメージがよくわからんとなったので適当に調べた感じ
Javaだとこんな感じらしい

セットアソシアティブ方式とダイレクトマッピング方式はこれがめちゃくちゃわかりやすかった
(アドレス空間/キャッシュ容量/ブロックサイズ/タグ/オフセット/インデックスとか用語から説明されてて助かる(基本と応用とったくせに完全に記憶から消失した))

オブジェクトヘッダにマークビットを用意することによってrace conditionを解消できる(外部に持つと更新状態が競合する可能性が存在する) ビットマップではなくバイトマップを使うとマーク処理のrace conditionが解消できるらしい(よくわかってない)

  • 安全性に関しての結論
    • コレクタはどこに格納された値であってもミューテータが保持する値を書き換えてはいけない(オブジェクトを移動させるどんなアルゴリズムも使えない, 移動するとポインタの修正が発生する)
      • オブジェクトのようなもの(のようなものって具体的になんなんだろう)のマークビットを変更することによってユーザのデータを破壊する恐れがある
    • ミューテータがコレクタのデータと干渉する機会を最小限にすることが有益
      • ヘッダワードに持つよりはオブジェクトと分離したデータ構造(これがバイトマップかな)に保存したほうがリスクが小さい
      • コレクタによるページング回数を減らすのも有益

マークスイープGCのメモリアロケータはフラグメンテーションを避ける為に一定サイズのブロックを連続して配置するらしい
(メモリアクセスのパターンが均一になってハードウェアのプリフェッチの機構と相性が良い)
マークフェーズのオブジェクトヘッダの読み取りによって発生するキャッシュミスの回数を減らす方法の一つにBiBoPというものがあるらしい
(同じページには単一の型のみを割り当てることによってオブジェクト毎の型情報をページ単位に圧縮が出来る)

GC本を読んで学ぶ1

ガベージコレクション: 自動的メモリ管理を構成する理論と実装を積ん読していたので消化するために前に読んでたメモをとりあえず残す

第一章

  • 動的なメモリ割り当てを利用することによって、コンパイル時に総サイズがわからないオブジェクトでも割り付けたり、開放したりすることができる
  • 動的に割り付けたオブジェクトはヒープに保存される
  • アクティベーションレコードとはなんぞ?(スタック内の割付らしい)
  • スタックフレームという領域があるらしい(よくわからん)
  • 静的割付(オブジェクトの名前をコンパイル時あるいはリンク時に決定するメモリ上の場所に束縛する方法)
  • なぜヒープへの割付が重要か?
    • オブジェクトのサイズを動的に決められる(配列の上限値をハードコードするとプログラムが壊れる恐れがある)
    • 再帰的なデータ構造を定義できる(リスト、ツリー、マップなど)
    • 新しく生成したオブジェクトを親の手続きに返せる(これによってファクトリーメソッドなどの技法を使える)
    • 関数の値として別の関数を返せる(所謂クロージャやサスペンション←初めて聞いた)
  • ヒープに割り付けたオブジェクトには参照を用いてアクセスする(ポインタ、ハンドル)
  • ハンドルを利用することで、オブジェクトの再配置が容易になる
  • 明示的解放(CでいうfreeやC++のdeleteなど)
  • 手動で解放する場合はプログラミングエラーを犯すリスクが有る(エラーは2種類)
    • 一つはメモリを解放するのが早すぎるエラーで、メモリへの参照が残っているのに誤って解放してしまうケース(このような参照を宙ぶらりんポインタと呼ぶ)
      • 例: A ref B ref C
      • Bを手動解放した場合にAはBへの参照が残り、Cは到達不能となり解放することもできなくなる
      • 宙ぶらりんのポインタを検出する方法の一つにファットポインタがある
        • ファットポインタには、ポインタが指しているオブジェクトとポインタ自身のバージョン番号を格納しておく
      • 参照をたどるような操作の際には、必ずポインタに格納してあるバージョン番号が、オブジェクトのバージョンと一致していることをチェックする
        • (この手法は、オーバーヘッドのため、デバッキングツールでしか使えないことが多い, また完全に信頼できる方法でもない)
    • 二つめはメモリリーク
      • 先程の例のように一箇所のメモリ解放の誤りが宙ぶらりんポインタ(A)とメモリリーク(C)の両方を引き起こしてしまうこともありうる
  • スレッドセーフなデータ構造にアクセスするアルゴリズムでは、さまざまな問題に対処する必要がある(デッドロック、ライブロック、ABAエラー)
  • ガベージコレクションは宙ぶらりんポインタが作られるのを防止する(オブジェクトが回収されるのは、到達可能なオブジェクトからのポインタが1つもなくなった時だけ)
  • GCは原則的にすべてのごみを解放することを保証する(すなわち到達不能なオブジェクトは全て最終的にはGCによって回収される)
  • 注意点は以下
    • 追跡型GC(Tracing collection)が用いる「ごみ」の定義は、決定可能なものであり、二度とアクセスされないオブジェクトすべてがごみとみなされるわけではない
    • GCの実装によっては、効率上の理由からオブジェクトの一部を回収しないことがあるということ
  • ガベージコレクタのみがオブジェクトを解放するので、二重解放問題は生じない(解放に関するすべての決定はコレクタが行う)
  • コレクタはヒープ内のオブジェクトの構造やオブジェクトにアクセスする可能性のあるスレッドに関するグローバルな情報を持っている
  • ガベージコレクションが望ましい理由は、コードを書くのが簡単になるということではない

    • 簡単にはなるが、それよりも重要なのはガベージコレクションによってメモリ管理の問題がインターフェイスから切り離せるということ
    • ガベージコレクションなしではメモリ管理の問題がプログラム中に散乱することになる
    • ガベージコレクションは再利用性を改善する、これが様々な分野でほぼすべての近代的な言語で必須とされている理由
    • ガベージコレクションはスペースリークが絶対にないという保証はできない
    • なぜなら、データ構造が到達可能なまま際限なく大きくなる場合(キャッシュ等)ガベージコレクションでは解決できない
    • さらに到達可能だが二度とアクセスされないような場合についても同様
  • GCに求められる要素

    • 安全性
      • 生きているオブジェクトの回収などを行わないこと
      • スループット
      • mark/cons比
    • 完全性
      • ヒープ中のごみがすべて回収されること, かなり難しい(常にそれが最適とは限らない)
      • 並行GC中に新たにでたごみを浮遊ごみと呼ぶ(そのため、並行GCの文脈では完全性をすべてのごみがいずれは回収されることとしたほうが良いことになる)
    • 即時性
      • GCアルゴリズムによって異なる、ここでもまた時間と空間のトレードオフが導かれる
    • 停止時間

AWS Batchのexporterを書いてみた

久しぶりのブログ更新
ここ最近読書によるインプット過多で全然アウトプットしていないので久しぶりにまともなものを書く

golangの勉強がてら書いてみたので見る人が見たら酷いかもしれない
シンプルにそれぞれのStateのJobをメトリクスとして取得してみた
Queue毎にカウントしただけのメトリクスを吐き出すことも考えたけど、PromQLで計算すれば出来るから一旦はこのままにしてみた
github.com

コード書いたついでに久しぶりにGrafanaも周りも触ったらかなり改善されていい感じだったので、仕事でもGrafana使いたい欲がすごい(今はDatadogを主に使ってる)

特定のIAMロールで実行できるSystemsManagerのドキュメントを制限する方法

特定のIAMロールで実行できるSystemsManagerのドキュメントを制限する方法

今後もまた実装したくなることがありそうなので、残しとく
結論から言うと大体下のようなIAM Policyで制限をかけることが可能

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ssm:StartSession",
            "Resource": [
                "arn:aws:ec2:*:*:instance/*"
            ],
            "Condition": {
                "BoolIfExists": {
                    "ssm:SessionDocumentAccessCheck": "true"
                },
                "StringLike": {
                    "ssm:resourceTag/Name": "FugaFuga"
                }
            }
        },
        {
            "Effect": "Allow",
            "Action": "ssm:StartSession",
            "Resource": "arn:aws:ssm:*:*:document/HogeHogeDocument"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ssm:TerminateSession",
                "ssm:ResumeSession"
            ],
            "Resource": "arn:aws:ssm:*:*:session/${aws:username}-*"
        }
    ]
}

やっている内容について書いていく

まずはこのstatement

{
    "Effect": "Allow",
    "Action": "ssm:StartSession",
    "Resource": [
        "arn:aws:ec2:*:*:instance/*"
    ],
    "Condition": {
        "BoolIfExists": {
            "ssm:SessionDocumentAccessCheck": "true"
        },
        "StringLike": {
            "ssm:resourceTag/Name": "FugaFuga"
        }
    }
}

単純にActionとResourceはSSMのStartSessionをどのEC2インスタンスに対してでも実行できるようにしている
その上でConditionを使用して対象の絞り込みをかける
BoolIfExistsのssm:SessionDocumentAccessCheckは下の公式を読むとよくわかるが、
AWS CLI でセッションドキュメントのアクセス許可チェックを適用する
通常ssm:StartSessionを許可した場合にSSM-SessionManagerRunShellがデフォルトで許可される
これが許可されてしまうとインスタンスに接続してごにょごにょ何でも出来てしまうのでまず明示的に許可していないドキュメントへのアクセスを遮断する為に必要
次にStringLikeで ssm:resourceTag/Name を絞っているが、これは特定の名前のインスタンスにのみ実行を許可するために使っている(今回であればNameがFugaFugaのインスタンスだけとなる)
これもよくある、この種類のインスタンスに対しては良いけど他へは実行できて欲しくないといったときに重宝する

次にこのstatement

{
    "Effect": "Allow",
    "Action": "ssm:StartSession",
    "Resource": "arn:aws:ssm:*:*:document/HogeHogeDocument"
}

前のstatementで明示的に許可をしていないドキュメントについてはアクセス出来なくさせたので、
ドキュメントを指定することによって実行を許可している
なんで前のstatementと分けて書いているかというと、
前のstatementはConditionのStringLikeでTagの一致を条件として使用しているので、ドキュメントにも同様のTagがないと弾かれてしまうようになる(ドキュメントのName tagにFugaFugaを設定するつもりはないので、今回は分けている)

最後にこのstatement

{
    "Effect": "Allow",
    "Action": [
        "ssm:TerminateSession",
        "ssm:ResumeSession"
    ],
    "Resource": "arn:aws:ssm:*:*:session/${aws:username}-*"
}

自分自身がStartSessionで作成したsessionに対してResumeとTerminateを出来るように許可している

書いてみると意外と単純だが、Conditionを扱うのが久しぶりだと若干頭を悩ませる