HackToTech

Hack To Technology

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