HackToTech

Hack To Technology

auroraでフェイルオーバーした際にコネクションの接続がぜんぜん切り替わらなくて色々見てたのでメモ

環境

  • Aurora postgresql
  • Spring Boot
    • driver は org.postgresql.driver
  • Doma

メモ

AWSのベストプラクティスにも書かれているやつで、targetServerTypeprimary に指定しておけばFOした際に自動的にコネクションが再接続される

jdbc:postgresql://myauroracluster.cluster-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432,myauroracluster.cluster-ro-c9bfei4hjlrd.us-east-1-beta.rds.amazonaws.com:5432/postgres?user=<primaryuser>&password=<primarypw>&loginTimeout=2&connectTimeout=2&cancelSignalTimeout=2&socketTimeout=60&tcpKeepAlive=true&targetServerType=primary

docs.aws.amazon.com

targetServerType = String

Allows opening connections to only servers with required state, the allowed values are any, primary, master, slave, secondary, preferSlave and preferSecondary. The primary/secondary distinction is currently done by observing if the server allows writes. The value preferSecondary tries to connect to secondary if any are available, otherwise allows falls back to connecting also to primary. jdbc.postgresql.org

ただAWS公式だと参照系クエリをreplicaに逃がす部分の話はなかったので、一応pgjdbcの実装を確認した github.com

HostStatus hostStatus = HostStatus.ConnectOK;
if (candidateHost.targetServerType != HostRequirement.any) {
  hostStatus = isPrimary(queryExecutor) ? HostStatus.Primary : HostStatus.Secondary;
}
GlobalHostStatusTracker.reportHostStatus(hostSpec, hostStatus);
knownStates.put(hostSpec, hostStatus);
if (!candidateHost.targetServerType.allowConnectingTo(hostStatus)) {
  queryExecutor.close();
  continue;
}

pgjdbc/ConnectionFactoryImpl.java at a966396c5e2ac3398ba5196d5e9baed705cf593e · pgjdbc/pgjdbc · GitHub

Tuple results = SetupQueryRunner.run(queryExecutor, "show transaction_read_only", true);

で接続先がreadonlyかどうかチェックしているらしい

pgjdbc/ConnectionFactoryImpl.java at a966396c5e2ac3398ba5196d5e9baed705cf593e · pgjdbc/pgjdbc · GitHub

  primary {
    public boolean allowConnectingTo(@Nullable HostStatus status) {
      return status == HostStatus.Primary || status == HostStatus.ConnectOK;
    }
  },
  secondary {
    public boolean allowConnectingTo(@Nullable HostStatus status) {
      return status == HostStatus.Secondary || status == HostStatus.ConnectOK;
    }
  },

allowConnectingTo でコネクションが接続先の条件(targetServerType で指定した HostStatus )にマッチしているかを確かめて、マッチしていなければクローズするみたいな実装にしているらしい

pgjdbc/HostRequirement.java at a966396c5e2ac3398ba5196d5e9baed705cf593e · pgjdbc/pgjdbc · GitHub

最終的に 書き込み用のコネクションプールの接続をjdbc:postgresql://<cluster_endpoiont>?targetServerType=primary
参照用のコネクションプールの接続をjdbc:postgresql://<cluster_readonly_endpoint>?targetServerType=secondary
にしていい感じになった
他にもベストプラクティスのパラメータ足したり、SpringBoot使ってるのでHikariCP周りの設定した

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