[アップデート] EKSでIAMロールを使ったPod単位のアクセス制御が可能になりました!

みなさん、こんにちは!
AWS事業本部の青柳@福岡オフィスです。

AWSのマネージドKubernetesサービスである「EKS」(Amazon Elastic Kubernetes Service) に、多くの人が待望していた (かもしれない) 機能がついに追加されました!

Amazon EKS Adds Support to Assign IAM Permissions to Kubernetes Service Accounts

タイトルを直訳すると「Amazon EKSがIAMアクセス許可をKubernetesサービスアカウントへ割り当てるサポートを追加」です。

今回のアップデートがどういうものであるのかの概要と、同時に公開されたチュートリアルを実際に行ってみた際の流れをご紹介します。

目次

今回のアップデートで何が変わったのか

サービスアカウントとはKubernetesに標準で備わっている認証認可関連の要素であり、Pod内のコンテナから各リソースへのアクセス制御の際に用いられます。

しかし、サービスアカウントはKubernetesの世界に閉じた概念であり、これまではAWSのIAMと連係することができませんでした。
そのため、EKS環境(あるいはKops等で構築したAWS上のKubernetes環境)においてPod内のコンテナからS3等のAWSリソースに対するアクセス制御を行う場合、大きな制約となっていました。

従来の方法

Podに対してAWSリソースに対するアクセス権限を与える場合の従来の方法は「IAMインスタンスプロファイルを使用してワーカーノードのEC2インスタンスにIAMロールを割り当てる」という方法でした。

EC2インスタンスに割り当てられたIAMロールに定義されたアクセス権限は、ワーカーノード上で動作するPodにも適用されます。
これによって、特定のアクセス権限をPod内コンテナに対して与えるという目的が達成されます。

しかし、この方法ですと、あるEC2インスタンス(ワーカーノード)上で動作する全てのPodに対して同一のアクセス権限を与えることになります。
もし異なるアクセス権限を与えたい場合には、別のEC2インスタンスを用意してIAMロールを割り当てる必要があります。
これだとPodの配置が柔軟に行えませんし、アクセス権限のパターンの数だけEC2インスタンスを用意することになるためリソース効率も悪くなります。

もしくは、EC2インスタンスに配置される可能性のある各Podに必要なアクセス権限を全て併せ持ったIAMロールを定義して、EC2インスタンスに割り当てるという方法もあります。
(上の図で言うと、Bucket-ABucket-B の両方にアクセス権限を持つIAMロールを1つだけ定義して Host-1 Host-2 へ一律に割り当てる)
しかし、この方法ではPodに本来必要のない過剰なアクセス権限を与えてしまうことになり、セキュリティ観点から推奨されないものになります。

今回のアップデートでできるようになったこと

今回のアップデートでは、下図のようにPod単位でIAMロールを割り当てることが可能になりました。

これにより、EC2インスタンス上で動作させる各Podのアクセス権限がどのようになっているかということに影響を受けずに、Podの配置が可能になりました。
また、IAMロールをPodに対して直接割り当てるため、アクセス制御の管理が直感的になり見通しが良くなります。

実は、今回のアップデート以前にも、同様のことを実現する kube2iamkiam などのOSSツールが公開されていました。
しかし、これらのツールはAWSクレデンシャルをKubernetesのメタデータを経由して渡すという手法を採用していたため、パフォーマンスの問題やセキュリティの懸念が指摘されていました。

今回のアップデートでは、標準化されたID連係の規格であるOpenID Connect (OIDC)を使った実装となっており、これらの懸念が解消されることが期待されます。

チュートリアルを行ってみる

前置きが長くなりましたが、それではチュートリアルを行ってみましょう。

AWS Blogにチュートリアルを含むブログ記事が公開されています。

Introducing Fine-Grained IAM Roles for Service Accounts | AWS Open Source Blog
(ページ中盤の「Example usage walkthrough」以降がチュートリアルとなっています)

今回は、このブログ記事に沿ってチュートリアルを行います。

0. 事前準備

チュートリアルを始める前に、AWS CLIとeksctlを最新化しておきましょう。
今回は、以下のバージョンで行いました。(9月5日時点の最新バージョン)

  • AWS CLI: 1.16.232
  • eksctl: 0.5.0

また、AWS CLIコマンドやeksctlコマンドが正しく実行されるように、プロファイルとリージョンを設定しておきます。
(環境変数 AWS_PROFILEAWS_REGION を設定するか、設定ファイル ~/.aws/config に記述します)

最後に、チュートリアルで使用する各種資産をGitHubリポジトリから入手しておきます。

$ git clone https://github.com/mhausenblas/s3-echoer.git

1. EKSクラスターとワーカーノードを作成する

eksctlコマンドを使用して、EKSクラスターとワーカーノードを作成します。

$ eksctl create cluster s3echotest

今回はチュートリアルということでクラスター名を除く全てのオプションを省略しましたので、作成されるクラスター・ワーカーノードはeksctlのデフォルト構成となっています。
(3つのAZにPublic/Privateの計6サブネット、ノードタイプ=m5.large、ノード数=2、etc.)

また、IAM Roles for Service Accounts機能を利用するにはKubernetesのバージョンが1.13以上である必要があります。
バージョン指定(--version)を省略した際のデフォルトは「バージョン1.13」ですので、こちらも問題ありません。

(※ チュートリアルではコマンドラインが eksctl create cluster --approve と記載されていますが、誤記だと思われます。クラスター名を省略するとランダムな名前になるため、後のコマンドと整合性が取れなくなります。また、--approve オプションは create cluster コマンドにはありません)

実行結果:

$ eksctl create cluster s3echotest
[ℹ]  using region ap-northeast-1
[ℹ]  setting availability zones to [ap-northeast-1c ap-northeast-1a ap-northeast-1d]
[ℹ]  subnets for ap-northeast-1c - public:192.168.0.0/19 private:192.168.96.0/19
[ℹ]  subnets for ap-northeast-1a - public:192.168.32.0/19 private:192.168.128.0/19
[ℹ]  subnets for ap-northeast-1d - public:192.168.64.0/19 private:192.168.160.0/19
[ℹ]  nodegroup "ng-0e53ed42" will use "ami-0a67c71d2ab43d36f" [AmazonLinux2/1.13]
[ℹ]  using Kubernetes version 1.13
[ℹ]  creating EKS cluster "s3echotest" in "ap-northeast-1" region
[ℹ]  will create 2 separate CloudFormation stacks for cluster itself and the initial nodegroup
[ℹ]  if you encounter any issues, check CloudFormation console or try 'eksctl utils describe-stacks --region=ap-northeast-1 --name=s3echotest'
[ℹ]  CloudWatch logging will not be enabled for cluster "s3echotest" in "ap-northeast-1"
[ℹ]  you can enable it with 'eksctl utils update-cluster-logging --region=ap-northeast-1 --name=s3echotest'
[ℹ]  2 sequential tasks: { create cluster control plane "s3echotest", create nodegroup "ng-0e53ed42" }
[ℹ]  building cluster stack "eksctl-s3echotest-cluster"
[ℹ]  deploying stack "eksctl-s3echotest-cluster"
[ℹ]  building nodegroup stack "eksctl-s3echotest-nodegroup-ng-0e53ed42"
[ℹ]  --nodes-min=2 was set automatically for nodegroup ng-0e53ed42
[ℹ]  --nodes-max=2 was set automatically for nodegroup ng-0e53ed42
[ℹ]  deploying stack "eksctl-s3echotest-nodegroup-ng-0e53ed42"
[✔]  all EKS cluster resource for "s3echotest" had been created
[✔]  saved kubeconfig as "/home/ubuntu/.kube/config"
[ℹ]  adding role "arn:aws:iam::XXXXXXXXXXXX:role/eksctl-s3echotest-nodegroup-ng-0e-NodeInstanceRole-LCRT367XZSI6" to auth ConfigMap
[ℹ]  nodegroup "ng-0e53ed42" has 0 node(s)
[ℹ]  waiting for at least 2 node(s) to become ready in "ng-0e53ed42"
[ℹ]  nodegroup "ng-0e53ed42" has 2 node(s)
[ℹ]  node "ip-192-168-55-117.ap-northeast-1.compute.internal" is ready
[ℹ]  node "ip-192-168-73-18.ap-northeast-1.compute.internal" is ready
[ℹ]  kubectl command should work with "/home/ubuntu/.kube/config", try 'kubectl get nodes'
[✔]  EKS cluster "s3echotest" in "ap-northeast-1" region is ready

EKSクラスターが作成されたので、マネジメントコンソール上でも確認してみましょう。

以前は無かった「OpenID Connect provider URL」という項目が表示されていることが確認できます。
(もし表示されていない場合は、AWS CLIやeksctlのバージョンを確認してください)

ついでに、このタイミングで kubectl cluster-infokubectl get nodes 等を実行して、EKSクラスターへの接続を確認しておくとよいでしょう。

2. IAMのIDプロバイダー(OIDCプロバイダー)を作成する

次に、AWSのIAMロールとKubernetesのサービスアカウントの橋渡しをしてくれる「OIDC」のIDプロバイダーを作成します。
これには、今回eksctlコマンドに新たに追加された eksctl utils associate-iam-oidc-provider コマンドを使用します。

$ eksctl utils associate-iam-oidc-provider --name s3echotest --approve

実行結果:

$ eksctl utils associate-iam-oidc-provider --name s3echotest --approve
[ℹ]  using region ap-northeast-1
[ℹ]  will create IAM Open ID Connect provider for cluster "s3echotest" in "ap-northeast-1"
[✔]  created IAM Open ID Connect provider for cluster "s3echotest" in "ap-northeast-1"

マネジメントコンソールで「IAM」→「IDプロバイダー」を開くと、OIDCのIDプロバイダーが作成されていることが確認できます。

「プロバイダーのURL」欄に、さきほどEKSクラスターで確認した「OpenID Connect provider URL」の値が設定されていることが分かります。
eksctlコマンドの実行時にプロバイダーURLの指定はありませんでしたが、EKSクラスター名から自動的に情報を引っ張ってきてくれるようです。

3. IAMロールとKubernetesサービスアカウントを作成する

次に、IAMロールとKubernetesサービスアカウントを作成します。

これらは eksctl create iamserviceaccount コマンドを使用して一度に作成することができます。

$ eksctl create iamserviceaccount \
                --name s3-echoer \
                --cluster s3echotest \
                --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess \
                --approve

--name で指定した名前は、サービスアカウント名、および、IAMロール名の一部に採用されます。

--attach-policy-arn にはIAMロールへアタッチするポリシーを指定します。
ここではAWS管理ポリシー AmazonS3FullAccess を指定しています。(S3へのフルアクセス権を与える)

(※ チュートリアルではコマンドラインの4行目の終わりに必要な \ が漏れています。そのまま実行するとエラーになりますので注意してください)

実行結果:

$ eksctl create iamserviceaccount \
>               --name s3-echoer \
>               --cluster s3echotest \
>               --attach-policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess \
>               --approve
[ℹ]  using region ap-northeast-1
[ℹ]  1 iamserviceaccount (default/s3-echoer) was included
[!]  serviceaccounts that exists in Kubernetes will be excluded, use --include-existing-serviceaccounts to override
[ℹ]  1 task: { 2 sequential sub-tasks: { create IAM role for serviceaccount "default/s3-echoer", create serviceaccount "default/s3-echoer" } }
[ℹ]  building iamserviceaccount stack "eksctl-s3echotest-addon-iamserviceaccount-default-s3-echoer"
[ℹ]  deploying stack "eksctl-s3echotest-addon-iamserviceaccount-default-s3-echoer"
[ℹ]  created serviceaccount "default/s3-echoer"

作成されたIAMロールを見てみましょう。

eksctl-(--nameで指定した名前)-addon-iamserviceaccount-de-Role1-(ランダム文字列) という命名規則でIAMロールが作成されています。
指定した AmazonS3FullAccess ポリシーがアタッチされていることも確認できます。

IAMロールの信頼関係は以下のようになっています。

信頼されたエントリーとしてOIDCプロバイダーのARNがセットされています。
また、AssumeRoleの条件としてKubernetesサービスアカウント名が指定されていることも確認できます。

今度は、Kubernetesサービスアカウントを見てみましょう。

$ kubectl get serviceaccounts
NAME        SECRETS   AGE
default     1         18m
s3-echoer   1         2m15s

default は最初からあるKubernetes標準のサービスアカウントで、新たに s3-echoer サービスアカウントが追加されています。

サービスアカウントの詳細は以下のようになっています。

$ kubectl get serviceaccounts s3-echoer --output yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::XXXXXXXXXXXX:role/eksctl-s3echotest-addon-iamserviceaccount-de-Role1-9VW8WH4S9BT
  creationTimestamp: "2019-09-05T11:49:14Z"
  name: s3-echoer
  namespace: default
  resourceVersion: "1734"
  selfLink: /api/v1/namespaces/default/serviceaccounts/s3-echoer
  uid: 2f18e2f2-cfd3-11e9-8817-06179e478c90
secrets:
- name: s3-echoer-token-2lz29

こちらには、アノテーションにIAMロールのARNがセットされていることが確認できます。

これで、IAMロールとKubernetesサービスアカウントが作成され、それぞれ相互に情報がセットされていることが分かりました。

4. S3バケットを作成する

テストに用いるS3バケットをAWS CLIで作成します。
バケット名は任意のものに変えてください。

$ TARGET_BUCKET=s3-echoer-bucket
$ aws s3api create-bucket \
            --bucket $TARGET_BUCKET \
            --create-bucket-configuration LocationConstraint=$(aws configure get region) \
            --region $(aws configure get region)

5. Kubernetesのリソース定義YAMLファイルを編集する

最初に入手したGitHubリポジトリに、Kubernetesのリソース定義YAMLファイルのテンプレートが用意されています。
こちらをコピーして編集しましょう。

$ cd s3-echoer
$ cp s3-echoer-job.yaml.template s3-echoer-job.yaml
$ vi s3-echoer-job.yaml

YAMLファイルの内容は以下のようになっています。(下記は編集後の内容になっています)

apiVersion: batch/v1
kind: Job
metadata:
  name: s3-echoer
spec:
  template:
    spec:
      serviceAccountName: s3-echoer
      containers:
      - name: main
        image: amazonlinux:2018.03
        command:
        - "sh"
        - "-c"
        - "curl -sL -o /s3-echoer https://github.com/mhausenblas/s3-echoer/releases/latest/download/s3-echoer-linux && chmod +x /s3-echoer && echo This is an in-cluster test | /s3-echoer s3-echoer-bucket"
        env:
        - name: AWS_DEFAULT_REGION
          value: "ap-northeast-1"
        - name: ENABLE_IRP
          value: "true"
      restartPolicy: Never

変更するポイントは2箇所です。

  • 15行目の最後にある TARGET_BUCKET の文字列部分を、作成したS3のバケット名に書き換えます。
  • 18行目の us-west-2 をS3バケットのリージョンに書き換えます。

ここまでできたら、全ての準備は完了です。

(ちょっと寄り道) リソース定義YAMLファイルで、何を定義しているの?

チュートリアルでは、S3へアクセスするテストアプリをKubernetesの「Jobリソース」として用意しています。
通常、KubernetesのPodは常駐型の動作をしますが、Jobリソースを使うとバッチジョブ型のPodを起動することができます。

※ Jobリソースについては、こちらのリンク先の説明が分かり易いと思います。
KubernetesのWorkloadsリソース(その2) | Think IT(シンクイット)

Jobリソースから起動されるPodが、どんな処理をするものなのか、ざっくり見ていきます。

  • コンテナイメージ amazonlinux を使ってコンテナを起動
  • GitHubから s3-echoer-linux という実行ファイルをダウンロードして s3-echoer にリネームして保存
  • s3-echoer に実行権限を付与して、ルートディレクトリに配置
  • s3-echoer を実行 (パラメータとしてS3バケット名を指定、また、This is an in-cluster test という文字列を標準入力から与える)

s3-echoer という実行ファイルについてですが、これは、このチュートリアルブログの執筆者の一人であるMichael Hausenblas氏が公開しているテスト用ツールです。
https://github.com/mhausenblas/s3-echoer

s3-echoer の動作内容は「パラメータ(第1引数)で指定されたS3バケットに空のオブジェクトを書き込む」というだけのシンプルな動作です。
(標準入力から文字列を与えると、オブジェクトの内容が与えられた文字列テキストになります)

s3-echoer は単体でもKubernetes上でも動作するツールとなっています。
実行時に環境変数 ENABLE_IRPtrue にセットすることで、IAM Roles for Service Accountsに対応した動作をします。
(※ 正確にはIRSAに対応した処理をしている訳ではなくて、AWS SDK for Goのバージョン関連の問題を吸収するための対応であるようです。興味のある方はソースコードを見てみてください)

今回のチュートリアルではこれを使ってIAM Roles for Service Accountsの動作をテストするという訳です。

6. Podを起動してS3へのアクセスを確認する

では、実際にPodがIAMロールで指定したアクセス権限をもってS3へアクセスできることをテストしましょう。

さきほどコピー・編集したリソース定義YAMLファイルを kubectl apply コマンドで適用します。

$ kubectl apply -f s3-echoer-job.yaml

投入されたJobリソースを確認します。

$ kubectl get jobs
NAME        COMPLETIONS   DURATION   AGE
s3-echoer   1/1           8s         11s

Jobリソースが起動したPodリソースを確認します。

$ kubectl get pods
NAME              READY   STATUS      RESTARTS   AGE
s3-echoer-mplhl   0/1     Completed   0          11s

Podの処理は数秒で終わるので、ステータスは Completed になっています。
(ステータスが Running のまま終わらないとか Error になる場合は、ここまでの手順でどこかが間違っている可能性があります)

S3バケットにオブジェクトが書き込まれたことを確認しましょう。

ちゃんとオブジェクトが作成されていました。(ファイル名の後ろの数字はUNIXタイムです)
つまり、Podに対して、S3バケットへの書き込みアクセス権限が正しく適用されているということです。

ファイルをダウンロードして中身も確認してみます。

$ aws s3 cp s3://s3-echoer-bucket/s3echoer-1567685163 .
download: s3://s3-echoer-bucket/s3echoer-1567685163 to ./s3echoer-1567685163
$ cat s3echoer-1567685163
This is an in-cluster test

ちゃんと書き込まれていますね。

これで、無事にチュートリアルを完走しました。

どんな仕組みなのか・・・少しだけ見てみましょう

さて、動作が確認できたところで、仕組みについても少しだけ追いかけてみましょう。
とは言っても、EKSがOIDCを使って内部的にどのような処理を行っているのか・・・については解説する紙面が足りない (私の理解も足りない!) ので、今回は外部から見える部分のみを確認してみることにします。

JobがPodを起動する際、どのようなリソース定義を行っているのか、内容をダンプしてみます。
(かなり分量がありますがご容赦ください)

$ kubectl get pods s3-echoer-mplhl --output yaml
apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubernetes.io/psp: eks.privileged
  creationTimestamp: "2019-09-05T12:05:58Z"
  generateName: s3-echoer-
  labels:
    controller-uid: 85781e04-cfd5-11e9-8817-06179e478c90
    job-name: s3-echoer
  name: s3-echoer-mplhl
  namespace: default
  ownerReferences:
  - apiVersion: batch/v1
    blockOwnerDeletion: true
    controller: true
    kind: Job
    name: s3-echoer
    uid: 85781e04-cfd5-11e9-8817-06179e478c90
  resourceVersion: "3227"
  selfLink: /api/v1/namespaces/default/pods/s3-echoer-mplhl
  uid: 857d935e-cfd5-11e9-8817-06179e478c90
spec:
  containers:
  - command:
    - sh
    - -c
    - curl -sL -o /s3-echoer https://github.com/mhausenblas/s3-echoer/releases/latest/download/s3-echoer-linux
      && chmod +x /s3-echoer && echo This is an in-cluster test | /s3-echoer s3-echoer-bucket
    env:
    - name: AWS_DEFAULT_REGION
      value: ap-northeast-1
    - name: ENABLE_IRP
      value: "true"
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::XXXXXXXXXXXX:role/eksctl-s3echotest-addon-iamserviceaccount-de-Role1-9VW8WH4S9BT
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
    image: amazonlinux:2018.03
    imagePullPolicy: IfNotPresent
    name: main
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: s3-echoer-token-2lz29
      readOnly: true
    - mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
      name: aws-iam-token
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  nodeName: ip-192-168-73-18.ap-northeast-1.compute.internal
  priority: 0
  restartPolicy: Never
  schedulerName: default-scheduler
  securityContext: {}
  serviceAccount: s3-echoer
  serviceAccountName: s3-echoer
  terminationGracePeriodSeconds: 30
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
  volumes:
  - name: aws-iam-token
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          audience: sts.amazonaws.com
          expirationSeconds: 86400
          path: token
  - name: s3-echoer-token-2lz29
    secret:
      defaultMode: 420
      secretName: s3-echoer-token-2lz29
status:
  conditions:
  - lastProbeTime: null
    lastTransitionTime: "2019-09-05T12:05:58Z"
    reason: PodCompleted
    status: "True"
    type: Initialized
  - lastProbeTime: null
    lastTransitionTime: "2019-09-05T12:06:06Z"
    reason: PodCompleted
    status: "False"
    type: Ready
  - lastProbeTime: null
    lastTransitionTime: "2019-09-05T12:06:06Z"
    reason: PodCompleted
    status: "False"
    type: ContainersReady
  - lastProbeTime: null
    lastTransitionTime: "2019-09-05T12:05:58Z"
    status: "True"
    type: PodScheduled
  containerStatuses:
  - containerID: docker://0897c48da182645826f3793e1a56fa9f244e7cfd7117f94f6099f9c01577ee85
    image: amazonlinux:2018.03
    imageID: docker-pullable://amazonlinux@sha256:9a712720ba49101d90a71d319b35d2bf578657cc114e48f13e15441bf3da679b
    lastState: {}
    name: main
    ready: false
    restartCount: 0
    state:
      terminated:
        containerID: docker://0897c48da182645826f3793e1a56fa9f244e7cfd7117f94f6099f9c01577ee85
        exitCode: 0
        finishedAt: "2019-09-05T12:06:05Z"
        reason: Completed
        startedAt: "2019-09-05T12:05:59Z"
  hostIP: 192.168.73.18
  phase: Succeeded
  podIP: 192.168.93.24
  qosClass: BestEffort
  startTime: "2019-09-05T12:05:58Z"

まず、46~52行目と72~84行目の volumesvolumeMounts の記述内容に着目します。

spec:
  containers:
・・・
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: s3-echoer-token-2lz29
      readOnly: true
    - mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
      name: aws-iam-token
      readOnly: true
・・・
  volumes:
  - name: aws-iam-token
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          audience: sts.amazonaws.com
          expirationSeconds: 86400
          path: token
  - name: s3-echoer-token-2lz29
    secret:
      defaultMode: 420
      secretName: s3-echoer-token-2lz29

この箇所は、IRSAを使用しない(ServiceAccountを指定しない)場合は以下のようになります。

spec:
  containers:
・・・
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: default-token-XXXXX
      readOnly: true
・・・
  volumes:
  - name: default-token-XXXXX
    secret:
      defaultMode: 420
      secretName: default-token-XXXXX

secret 経由で受け渡しているサービスアカウントのトークン名が両者で異なることが分かります。
(ServiceAccountを指定しない場合は default というサービスアカウントが使用されます)

また、IRSA使用時には aws-iam-token というトークンが projected 経由で受け渡されています。
どうやら Service Account Token Volume Projection という仕組みが使われているようです。

次に、36~39行目の env の部分です。

spec:
  containers:
・・・
    env:
    - name: AWS_DEFAULT_REGION
      value: ap-northeast-1
    - name: ENABLE_IRP
      value: "true"
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::XXXXXXXXXXXX:role/eksctl-s3echotest-addon-iamserviceaccount-de-Role1-9VW8WH4S9BT
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token

この箇所は、IRSAを使用しない(ServiceAccountを指定しない)場合は以下のようになります。

spec:
  containers:
・・・
    env:
    - name: AWS_DEFAULT_REGION
      value: ap-northeast-1
    - name: ENABLE_IRP
      value: "true"

IRSA使用時には、Jobの定義YAMLには記述されていなかった AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILE という2つの環境変数が追加指定されています。

指定されたIAMロールのARNと、OIDCを使って取得されたSTSトークンの場所が、環境変数経由でPod内コンテナに情報伝達されているようです。

このように、IRSAが使用される場合は「Pod定義に対して必要な設定が自動的に追加(注入/injection)される」ということを覚えておくとよいかと思います。

追加検証:複数のIAMロールと複数のPodの組み合わせでIRSAの動作を確認する

長丁場になりつつありますが、最後に追加で検証を行ってみたいと思います。

チュートリアルでは1つのIAMロールを1つのPodに割り当てるという内容でしたが、これだと「Pod単位のアクセス制御」が本当に行えるのかイマイチ分からないのではないかと思います。

そこで、最初に示した図のように、2つのS3バケットに対するアクセス権限を持つ2種類のIAMロールを用意した上で、複数のPodに別々のアクセス権限を適用できることを確認します。

EKSクラスター、ワーカーノード、OIDCプロバイダー

チュートリアルで作成したものをそのまま利用します。

S3バケットの作成

S3バケットを2つ作成します。

$ aws s3api create-bucket \
            --bucket s3-echoer-bucket-a \
            --create-bucket-configuration LocationConstraint=$(aws configure get region) \
            --region $(aws configure get region)

$ aws s3api create-bucket \
            --bucket s3-echoer-bucket-b \
            --create-bucket-configuration LocationConstraint=$(aws configure get region) \
            --region $(aws configure get region)

IAMポリシーの作成

作成した2つの各S3バケットに対するフルアクセス権限を定義したIAMポリシーを作成します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::s3-echoer-bucket-a",
                "arn:aws:s3:::s3-echoer-bucket-a/*"
            ]
        }
    ]
}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::s3-echoer-bucket-b",
                "arn:aws:s3:::s3-echoer-bucket-b/*"
            ]
        }
    ]
}
$ aws iam create-policy --policy-name s3-echoer-a-policy --policy-document file://s3-echoer-a-policy.json
$ aws iam create-policy --policy-name s3-echoer-b-policy --policy-document file://s3-echoer-b-policy.json

IAMロール・Kubernetesサービスアカウントの作成

作成した2つの各IAMポリシーをアタッチしたIAMロール、および、IAMロールに対応するKubernetesサービスアカウントを作成します。

$ eksctl create iamserviceaccount \
                --name s3-echoer-a \
                --cluster s3echotest \
                --attach-policy-arn arn:aws:iam::XXXXXXXXXXXX:policy/s3-echoer-a-policy \
                --approve

$ eksctl create iamserviceaccount \
                --name s3-echoer-b \
                --cluster s3echotest \
                --attach-policy-arn arn:aws:iam::XXXXXXXXXXXX:policy/s3-echoer-b-policy \
                --approve

Jobリソースの作成

以下の2つのJobリソースを作成します。

  • s3-echoer-a-to-a-job
    • サービスアカウント: バケットAへのアクセス権限を持つIAMロールに対応するサービスアカウント
    • 処理内容: バケットAに対してオブジェクトの作成を試みる
  • s3-echoer-a-to-b-job
    • サービスアカウント: バケットAへのアクセス権限を持つIAMロールに対応するサービスアカウント
    • 処理内容: バケットBに対してオブジェクトの作成を試みる
apiVersion: batch/v1
kind: Job
metadata:
  name: s3-echoer-a-to-a
spec:
  template:
    spec:
      serviceAccountName: s3-echoer-a
      containers:
      - name: main
        image: amazonlinux:2018.03
        command:
        - "sh"
        - "-c"
        - "curl -sL -o /s3-echoer https://github.com/mhausenblas/s3-echoer/releases/latest/download/s3-echoer-linux && chmod +x /s3-echoer && echo My name is s3-echoer-a-to-a | /s3-echoer s3-echoer-bucket-a"
        env:
        - name: AWS_DEFAULT_REGION
          value: "ap-northeast-1"
        - name: ENABLE_IRP
          value: "true"
      restartPolicy: Never
apiVersion: batch/v1
kind: Job
metadata:
  name: s3-echoer-a-to-b
spec:
  template:
    spec:
      serviceAccountName: s3-echoer-a
      containers:
      - name: main
        image: amazonlinux:2018.03
        command:
        - "sh"
        - "-c"
        - "curl -sL -o /s3-echoer https://github.com/mhausenblas/s3-echoer/releases/latest/download/s3-echoer-linux && chmod +x /s3-echoer && echo My name is s3-echoer-a-to-b | /s3-echoer s3-echoer-bucket-b"
        env:
        - name: AWS_DEFAULT_REGION
          value: "ap-northeast-1"
        - name: ENABLE_IRP
          value: "true"
      restartPolicy: Never

動作の確認

まず、s3-echoer-a-to-a-job の方から実行してみます。

$ kubectl apply -f s3-echoer-a-to-a-job.yaml
job.batch/s3-echoer-a-to-a created
$ kubectl get pods
NAME                     READY   STATUS      RESTARTS   AGE
s3-echoer-a-to-a-ddglm   0/1     Completed   0          13s

Podは正常に処理完了したようです。

S3バケットに作成されたオブジェクトも確認してみます。

$ aws s3 ls s3://s3-echoer-bucket-a
2019-09-08 16:14:03         23 s3echoer-1567926840
$ aws s3 cp s3://s3-echoer-bucket-a/s3echoer-1567926840 .
download: s3://s3-echoer-bucket-a/s3echoer-1567926840 to ./s3echoer-1567926840
$ cat s3echoer-1567926840
My name is s3-echoer-a-to-a

オブジェクトが正常に作成されたことが確認できました。

次に、s3-echoer-a-to-b-job の方を実行してみます。

$ kubectl apply -f s3-echoer-a-to-b-job.yaml
job.batch/s3-echoer-a-to-b created
$ kubectl get pods
NAME                     READY   STATUS      RESTARTS   AGE
s3-echoer-a-to-a-ddglm   0/1     Completed   0          4m6s
s3-echoer-a-to-b-6vnbh   0/1     Error       0          10s

Podがエラーとなっています。

Podのログを確認してみます。

$ kubectl logs s3-echoer-a-to-b-6vnbh
Uploading user input to S3 using s3-echoer-bucket-b/s3echoer-1567927074

2019/09/08 07:17:57 Can't upload to S3: AccessDenied: Access Denied
        status code: 403, request id: 50424DF5B99506BE, host id: DqSeg26SkZqQ6NZOj+czciEfEWvBToUayPc0jaMID5v94yLmAl0/gNP3Idr97foLhqGeo5dZDrE=

アクセス権限が無いためオブジェクトを作成できなかったことが分かります。

同様にして、Job s3-echoer-b-to-b-jobs3-echoer-b-to-a-job を作成して実行すると、前者はS3バケットへアクセスすることができ、後者はアクセス不可になることが確認できるかと思います。(Job定義および実行結果は割愛します)

  • s3-echoer-b-to-b-job
    • サービスアカウント: バケットBへのアクセス権限を持つIAMロールに対応するサービスアカウント
    • 処理内容: バケットBに対してオブジェクトの作成を試みる
  • s3-echoer-b-to-a-job
    • サービスアカウント: バケットBへのアクセス権限を持つIAMロールに対応するサービスアカウント
    • 処理内容: バケットAに対してオブジェクトの作成を試みる

このように、IAM Role for Service Accountsを使用することにより、Pod単位でIAMロールを割り当ててアクセス制御ができることが確認できました。

おわりに

以上、EKSの「IAM Roles for Service Accounts」アップデートの概要と使用方法についてご紹介しました。

AWS側のIAMロールとKubernetes側のサービスアカウントが1対1で対応しているため、アクセス権限の管理が分かり易く行えるのではないかと思います。

eksctlコマンドを使うことでAWSとKubernetesの設定が簡略化できる点や、裏で動いているSTSトークンなどの設定まわりを自動追加してくれる点など、設定まわりも分かり易くなっているのではないでしょうか。

EKSを使う上で非常に有用なアップデートだと思いますので、みなさんも是非、本ブログを参考に試してみてください。