2017年3月24日金曜日

【技術メモ】SpringCloudAWS が ECS TaskRole を使わない問題を突破する

サーバー運用っていいですよね。(1ヶ月20日ぶり2回目)

最近作っているサービスはその性質も相まってコンテナでの動作と相性が良さそうなので、基本的には Docker Container として動くことを前提に環境周りを構築しました。

すいません。うそです。適当に言いました。

実際の運用はご多聞に洩れず AWS ECS を使っています。
が、あまり情報が無いのも事実で何か起こる度に調査やら前に進まない試行錯誤やらで MP aka 精神力 を激しく削られるのも事実ではあります。

なにより日本語の情報が「とりあえず動かしてみた」的なものしかなく、実際ハマった内容なんかは大体 StackOverflow あたりを探すか SpringBoot 関連の issue 漁るかくらいしか方法がない。

とはいえ、実務上での利用が増えてこないとこういう事例自体出てこないと思うので、では実務上で使ってハマったのだから公開していこう、とそんな感じです。

さて。

今回は Spring Cloud AWS を利用していてある機能が動かなかったことから始まりました。
Spring Cloud for Amazon Web Services

どうやってその現象の理由を探したのか、結局どういう方法をとって解決したのかを遡ってみたいと思います。


【一日目】 ECS で動作している Container に権限が足りていないことが発覚

サーバ側ソフトのバージョンアップ版を作っていて、何故か特定の Permission が足りなくて AWS SDK for Java の Client がコケる。

手元の開発機では発生しない。

開発機と同じように ECS TaskDefinition に AWS の Credential を環境変数に設定すると動作する。

どうも認証周りが意図通り通っていないっぽい。

原因を調べて ECS TaskRole の内容で認証するようにしたい。

状況を確認した手順

とりあえず各種環境の状態を確認することにした。

Docker Container 上で定義される環境変数を確認する手順
まずは一番深いところにある ECS Instance 上で起動している Docker Image の中から確認する。
AmazonWebService: Amazon EC2 Container Service - タスク用の IAM ロール について

このドキュメントによると
$ curl 169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
で認証情報をクエリ出来るらしい。

内容を確認するとどうも問題なさそう。

正しい RoleARN が引けてる。この IAM Policy Role は必要なものは正しく割り当たっている。

Docker Container で定義される環境変数を確認する手順
次に ECS Cluster として起動している EC2 Instance の環境を確認する。
AmazonWebService: Amazon Elastic Compute Cloud Amazon EC2 の IAM ロール - インスタンスメタデータからセキュリティ認証情報を取得する

このドキュメントによると
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/適当なRole
でインスタンスメタデータから認証情報が引けるらしい。

ちなみに、
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials
だけで実行するととりあえず呼び出せる Role が出てくる。

一日目の考察

EC2 Instance 自体は特に他のサービスやリソースにアクセスする権限を持っている必要は無いので内容的には問題無い。

のだが...

この Instance に割当てた IAMRole が気になったので、ECS TaskRole に必要な Permission を設定してみたところ、期待している動作をしているように見える。

なぜか ECR TaskDefinition で指定している ECR TaskRole ではなく EC2 AutoScaling に指定している ContainerInstance 用の IAMRole が使われている?

時間切れ。今日は終了。

【二日目】 Spring Cloud AWS が ECS TaskRole を利用するために必要なことの追跡

二日目の進み方は結果をどうしたいか、から探しはじめ、どうするとそうなるのかを逆に辿った。

二日目の目標探し

個人的な想いとして ECR TaskRole に設定された IAMRole で動作して欲しい。
でも Spring Cloud AWS が EC2ContainerCredentialsProviderWrapper を AWSCredentialsProviderChain に入れてくれない。

欲しい CredentialsProvider は EC2ContainerCredentialsProviderWrapper。
public class EC2ContainerCredentialsProviderWrapper implements AWSCredentialsProvider {
...
    private AWSCredentialsProvider initializeProvider() {
        try {
            return (AWSCredentialsProvider)(System.getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != null?new ContainerCredentialsProvider():new InstanceProfileCredentialsProvider());
        } catch (SecurityException var2) {
...
            return new InstanceProfileCredentialsProvider();
        }
    }
...
}

望む状態の AWSCredentialsProviderChain が得られるかどうか

Spring Cloud AWS で AWSCredentialsProviderChain を作っている部分を探した。 CredentialsProviderFactoryBean というクラスに行き着く。

どうも Constractor に指定されている delegates が空になれば EC2ContainerCredentialsProviderWrapper を含んだ DefaultAWSCredentialsProviderChain を使ってくれそう。
public class CredentialsProviderFactoryBean extends AbstractFactoryBean<AWSCredentialsProvider> {
...
 public CredentialsProviderFactoryBean(List<AWSCredentialsProvider> delegates) {
  Assert.notNull(delegates, "Delegates must not be null");
// ここが空になるような状態にしたい
  this.delegates = delegates;
 }
...
 @Override
 protected AWSCredentialsProvider createInstance() throws Exception {
  AWSCredentialsProviderChain awsCredentialsProviderChain;
// this.delegates を空にした結果、この下の判定で EC2ContainerCredentialsProvider が含まれている 
// DefaultAWSCredentialsProviderChain が使えるようになって ECRTaskRole に設定された IAMRole が使えるようになる
  if (this.delegates.isEmpty()) {
   awsCredentialsProviderChain = new DefaultAWSCredentialsProviderChain();
  }else{
   awsCredentialsProviderChain = new AWSCredentialsProviderChain(this.delegates.toArray(new AWSCredentialsProvider[this.delegates.size()]));
  }

  awsCredentialsProviderChain.setReuseLastProvider(false);
  return awsCredentialsProviderChain;
 }
}

条件を成立させるための抜け道を探す

スタックを辿るとどうやら CredentialsProviderFactoryBean を呼ぶ前に ContextCredentialsAutoConfiguration の registerBeanDefinitions が通っているようだ。
public class ContextCredentialsAutoConfiguration {
 public static class Registrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
...
  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
   registerCredentialsProvider(registry, this.environment.getProperty("cloud.aws.credentials.accessKey"),
     this.environment.getProperty("cloud.aws.credentials.secretKey"),
     this.environment.getProperty("cloud.aws.credentials.instanceProfile", Boolean.class, true) &&
       !this.environment.containsProperty("cloud.aws.credentials.accessKey"),
     this.environment.getProperty("cloud.aws.credentials.profileName", DEFAULT_PROFILE_NAME),
     this.environment.getProperty("cloud.aws.credentials.profilePath"));
  }
 }
}
registerCredentialsProvider の中にいる awsCredentialsProviders.add を一度も実行しなければ CredentialsProviderFactoryBean の constractor 引数 delegates は空になるので、その条件を設定して DefaultAWSCredentialsProviderChain を使ってもらおう。

サラッと書いたがここが本題。コードとしては強引だが ECS の機能から考えるとこう動くのが良いと思うので。
public final class ContextConfigurationUtils {
...

 public static void registerCredentialsProvider(BeanDefinitionRegistry registry, String accessKey, String secretKey, boolean instanceProfile, String profileName, String profilePath) {
  BeanDefinitionBuilder factoryBeanBuilder = BeanDefinitionBuilder.genericBeanDefinition(CredentialsProviderFactoryBean.class);

  ManagedList<BeanDefinition> awsCredentialsProviders = new ManagedList<>();

  if (StringUtils.hasText(accessKey)) {
...
   awsCredentialsProviders.add(provider.getBeanDefinition());
  }

  if (instanceProfile) {
   awsCredentialsProviders.add(BeanDefinitionBuilder.rootBeanDefinition(InstanceProfileCredentialsProvider.class).getBeanDefinition());
  }

  if (StringUtils.hasText(profileName)) {
...
   awsCredentialsProviders.add(builder.getBeanDefinition());
  }

  factoryBeanBuilder.addConstructorArgValue(awsCredentialsProviders);

  registry.registerBeanDefinition(CredentialsProviderFactoryBean.CREDENTIALS_PROVIDER_BEAN_NAME, factoryBeanBuilder.getBeanDefinition());

  AmazonWebserviceClientConfigurationUtils.replaceDefaultCredentialsProvider(registry, CredentialsProviderFactoryBean.CREDENTIALS_PROVIDER_BEAN_NAME);
 }
    ...
}

対策方法

具体的には
cloud.aws.credentials.instanceProfile=false
cloud.aws.credentials.profileName=
# ここから下は未定義の状態にする。""でも空でもダメ。
# cloud.aws.credentials.accessKey=
# cloud.aws.credentials.secretKey=
という状態にする。

二日間の考察

Bean の Exclude も試してみたけどどうもうまく行っていない。理由がわからないので若干モヤモヤする。

とはいえ Spring Cloud AWS の master ブランチ(2017-03-13現在)には既にこれの対応が merge されているので、 CredentialsProvider 周りに理解を深めたついでに対応されるまでの期間だけ発動する黒魔術を置いておくことでとりあえず終わりにしておきたい。
Support IAM Roles for Tasks when running in AWS ECS #197

ちなみに、ローカルで実行するときは AWS CLI で config して cloud.aws.credentials.profileName に適当な profile を設定するとそれが使われる。

結論

我が爆裂魔法は最強にして最高(というネタにしたかった


0 件のコメント :

コメントを投稿