HackToTech

Hack To Technology

semverが正しいかを確認する

仕事でタグの棚卸しをしていてこのsemver記法的に正しいんだっけみたいなのぱぱっと見る必要があったので備忘録がてら残しとく

オンラインでぱぱっと見れたらいいんだけど、
なんか探し方が悪かったのかsemverとは何かみたいなサイトばっかりひっかかったので
大人しくnpmのsemverパッケージ使う方が早そうだった
www.npmjs.com
package.jsonとか書いてるならnpm semantic version calculatorとかのほうが良い

# 適当に入れる(node.js入ってる前提)
$ npm install semver
$ node
> const semver = require('semver')
// ただ正しいかどうかならvalidでnullが返ってこなければOK
> semver.valid('1.0.0')
'1.0.0'
> semver.valid('1.2.3.4')
null


// どこがどれだっけ的なのを確認するならparseで見るのが手っ取り早かった
> semver.parse('1.0.0-beta')
SemVer {
  options: {},
  loose: false,
  includePrerelease: false,
  raw: '1.0.0-beta',
  major: 1,
  minor: 0,
  patch: 0,
  prerelease: [ 'beta' ],
  build: [],
  version: '1.0.0-beta'
}

Pythonで辞書の配列から新たに辞書を作成する

pythonのreduceたまにどこにあるかよくわかんなくて迷う

from functools import reduce

ld = [{'Key': 'Key1', 'Value': 'Value1'}, {'Key': 'Key2', 'Value': 'Value2'}, {'Key': 'Key3', 'Value': 'Value3'}]
reduce(lambda acc, x: dict(acc, **{x['Key']: x['Value']}) , ld, {})
# {'Key1': 'Value1', 'Key2': 'Value2', 'Key3': 'Value3'}

ld2 = [{'Key1': 'Value1'}, {'Key2': 'Value2'}, {'Key3': 'Value3'}]
reduce(lambda acc, x: dict(acc, **x), ld2, {})
# {'Key1': 'Value1', 'Key2': 'Value2', 'Key3': 'Value3'}

FargateでAWS CDKを実行して動的にクロスアカウントの別リージョンにデプロイしようとしたらハマった話

あんまりケースとしてはなさそうだけどCDKの実行基盤でFargateを採用してみた
なんでそんなことしてたかの詳細は割愛するとして、
自分同様DockerImageにCDKの諸々を固めて
cdk --profile なんたら --role-arn なんたら2 deploy
みたいなことをFargateでやっていて且つ他アカウントの別リージョンにデプロイする人の助けにでもなれば報われる

そもそも何が起こるか

他アカウントの別リージョンにデプロイするはずが、
他アカウントのデプロイ元のFargateが実行されているリージョンでデプロイがされる

例えば、Aアカウントの東京リージョンでFargateを立ち上げて
そこからBアカウントのオレゴンリージョンにデプロイとかしようとするとBアカウントの東京リージョンにデプロイされるという話

原因調査

上でdeploy時にprofileを指定している通り
~/.aws/config に↓のようなconfigを入れています
~/.aws/credentials は空ファイルにしてます

[profile xxx]
region = us-west-2
role_arn = arn:aws:iam::xxxxxxxxxx:role/DeployRole
role_session_name = deploy
credential_source = EcsContainer

環境変数にも似たようなものを入れてます
(というか↑のconfigを動的に生成する為に環境変数経由でデプロイ先のアカウントとリージョンをコンテナに伝えてます)

ACCOUNT_ID=xxxxxxxxxx
ACCOUNT_REGION=us-west-2

Fargate起動時にはプロセスにはこんな感じの環境変数が自動的に設定されています

HOSTNAME=ip-x-y-z-zz.ap-northeast-1.compute.internal
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/v2/credentials/xxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_DEFAULT_REGION=ap-northeast-1
AWS_EXECUTION_ENV=AWS_ECS_FARGATE
AWS_REGION=ap-northeast-1
ECS_CONTAINER_METADATA_URI=http://169.254.170.2/v3/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ECS_CONTAINER_METADATA_URI_V4=http://169.254.170.2/v4/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

cdkのdoctorで見ると↓みたいなのが出てきます

cdk --profile xxx --role-arn arn:aws:iam::xxxxxxxxxx:role/DeployRoleForCloudFormation doctor
:information_source: CDK Version: 1.110.1 (build 0028d09)
:information_source: AWS environment variables:
  - AWS_EXECUTION_ENV = AWS_ECS_FARGATE
  - AWS_CONTAINER_CREDENTIALS_RELATIVE_URI = /v2/credentials/xxxxxxxxxxxxxxxxxxxxxx
  - AWS_DEFAULT_REGION = ap-northeast-1
  - AWS_REGION = ap-northeast-1
  - AWS_STS_REGIONAL_ENDPOINTS = regional
  - AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1
  - AWS_SDK_LOAD_CONFIG = 1
:information_source: No CDK environment variables
END

勘の良い人であればなんか怪しいやつらがいることに気がつきますね?
こいつらです

- AWS_DEFAULT_REGION = ap-northeast-1
- AWS_REGION = ap-northeast-1

もしこいつらが原因ならリージョンとアカウントをCDKの bin/stack.tsに環境変数の値で直接渡せば解決するんじゃね?と考えました

↓は公式のサンプルでとりあえずこの通りに渡すことにしました
Environments - AWS Cloud Development Kit (CDK)

new MyDevStack(app, 'dev', { 
  env: { 
    account: process.env.CDK_DEFAULT_ACCOUNT, 
    region: process.env.CDK_DEFAULT_REGION 
}});

そして一番上の方で設定していた環境変数とは別に↓のような感じでCDK向けに新たに環境変数を追加しました

CDK_DEFAULT_ACCOUNT="${ACCOUNT_ID}"
CDK_DEFAULT_REGION="${ACCOUNT_REGION}"

そしてサンプルのように渡した上でdoctorでも確認をし問題なく設定されていることを確認しました

cdk --profile xxx --role-arn arn:aws:iam::xxxxxxxxxx:role/DeployRoleForCloudFormation doctor
...
:information_source: CDK environment variables:
  - CDK_DEFAULT_ACCOUNT = xxxxxxxxxx
  - CDK_DEFAULT_REGION = us-west-2

この状態でbootstrapを叩くと...

cdk --profile xxx --role-arn arn:aws:iam::xxxxxxxxxx:role/DeployRoleForCloudFormation bootstrap
CDK_DEFAULT_ACCOUNT:xxxxxxxxxx //これはconsole.logを仕込んだ
CDK_DEFAULT_REGION:ap-northeast-1 // これはconsole.logを仕込んだ

CDK_DEFAULT_REGION:ap-northeast-1😇

なんか勝手に上書きされました
先程の勘が良い人であれば、なんか上書きしそうなやつがいたことに心当たりがありますね?
こいつらです

- AWS_DEFAULT_REGION = ap-northeast-1
- AWS_REGION = ap-northeast-1

ここまでくるとCDKの内部の挙動について読む必要が出てくるので読んでいきます
(下記の推測が間違っている可能性もなくはないので、最終的には最新のコードを読んでください)

CDK_DEFAULT_REGIONここで定義されています

/**
 * Environment variable set by the CDK CLI with the default AWS region.
 */
export const DEFAULT_REGION_ENV = 'CDK_DEFAULT_REGION';

この変数で値を設定しているところを探します

async function populateDefaultEnvironmentIfNeeded(aws: SdkProvider, env: { [key: string]: string | undefined}) {
  env[cxapi.DEFAULT_REGION_ENV] = aws.defaultRegion;
  ...
}

aws.defaultRegionを探しますが、そもそもawsは引数で渡されているのでそちらから探します
関数自体の呼び出しはexecProgramの中で呼ばれています

export async function execProgram(aws: SdkProvider, config: Configuration): Promise<cxapi.CloudAssembly> {
  const env: { [key: string]: string } = { };

  const context = config.context.all;
  await populateDefaultEnvironmentIfNeeded(aws, env);
  ...
}

execProgramはCloudExecutableの内部で呼ばれていることが以下の二箇所からわかります

ここ

async function initCommandLine() {
  ...
  const sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({
    profile: configuration.settings.get(['profile']),
    ec2creds: argv.ec2creds,
    httpOptions: {
      proxyAddress: argv.proxy,
      caBundlePath: argv['ca-bundle-path'],
    },
  });

  const cloudFormation = new CloudFormationDeployments({ sdkProvider });

  const cloudExecutable = new CloudExecutable({
    configuration,
    sdkProvider,
    synthesizer: execProgram,
  });
  ...
}

ここ

export class CloudExecutable {
  ...
  private async doSynthesize(): Promise<CloudAssembly> {
    ...
    while (true) {
      const assembly = await this.props.synthesizer(this.props.sdkProvider, this.props.configuration);
      ...
    }
    ...
  }
  ...
}

CloudExecutableのsdkProviderはinitCommandLine側で作られたものであることも同時にわかるので
SdkProvider.withAwsCliCompatibleDefaultsを探します

export class SdkProvider {
  ...
  public static async withAwsCliCompatibleDefaults(options: SdkProviderOptions = {}) {
    ...
    const region = await AwsCliCompatible.region({
      profile: options.profile,
      ec2instance: options.ec2creds,
    });

    return new SdkProvider(chain, region, sdkOptions);
  }
  ...

  public constructor(
    private readonly defaultChain: AWS.CredentialProviderChain,
    /**
     * Default region
     */
    public readonly defaultRegion: string,
    private readonly sdkOptions: ConfigurationOptions = {}) {
  }

   ...
}

defaultRegionはAwsCliCompatible.regionで取得した値を使用していることがわかったので更に探します

export class AwsCliCompatible {
  ...
  public static async region(options: RegionOptions = {}): Promise<string> {
    ...
    let region = process.env.AWS_REGION || process.env.AMAZON_REGION ||
      process.env.AWS_DEFAULT_REGION || process.env.AMAZON_DEFAULT_REGION;

    while (!region && toCheck.length > 0) {
      const opts = toCheck.shift()!;
      if (await fs.pathExists(opts.filename)) {
        const configFile = new SharedIniFile(opts);
        const section = await configFile.getProfile(opts.profile);
        region = section?.region;
      }
    }

    ...
    return region;
  }
}

ようやく終わりが見えました
ここまで来ると設定されるリージョンの優先順位は

process.env.AWS_REGION >
process.env.AMAZON_REGION >
process.env.AWS_DEFAULT_REGION >
process.env.AMAZON_DEFAULT_REGION >
プロファイルのセクションにあるリージョン >
その他の設定(ここはコードを見てください)

であることがわかりました
ここで元々の設定を振り返ると明らかに2つの環境変数が悪さをしていることがわかります

- AWS_DEFAULT_REGION = ap-northeast-1
- AWS_REGION = ap-northeast-1

しかしながらこれらの環境変数をunsetして実行するのが良いとは言いづらいので、
(Fargate側で何かに利用している可能性も否めない)
最終的に下記2つの環境変数を利用することをやめて、
元々自分で設定していた環境変数をbin/stack.tsで利用することで上書きを回避し問題なく動かす事ができました

CDK_DEFAULT_ACCOUNT
CDK_DEFAULT_REGION

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というものがあるらしい
(同じページには単一の型のみを割り当てることによってオブジェクト毎の型情報をページ単位に圧縮が出来る)