Amazon EKSでkube2iamを使う手順

# はじめに

「Kubernetes on AWS」な環境においてIAMと連係したアクセス制御を行う仕組みとして、「kube2iam」や「kiam」などのOSSが開発されています。

それらのうちkube2iamについて、Kops等を使用して構築したKubernetesクラスタに対するkube2iamの導入手順は、既にそれなりの数の情報を見ることができます。
しかし、Amazon EKS環境については具体的な情報を見かけませんでしたので、試してみた過程をまとめました。
(既に情報が出ていればすみません)


### 参考にしたドキュメント・記事

kube2iam 公式ドキュメント
https://github.com/jtblin/kube2iam

AWS Workshop for Kubernetes / 402 - Authentication and Authorization with Kubernetes
https://github.com/aws-samples/aws-workshop-for-kubernetes/tree/master/04-path-security-and-networking/402-authentication-and-authorization

AWS ECSのタスク同様に、KubernetesでもPod毎にIAMロールを設定する仕組みと方法
https://qiita.com/mumoshu/items/3dd5179efce8f626a99c

kube2iam で Kubernetes の Pod に IAM role を割り当てる
https://blog.shiftky.net/integrate-aws-iam-with-kubernetes-using-kube2iam/


# kube2iamとは？

KubernetesのPodからAWSリソース(S3等)へアクセスする方法は、主に以下の2通りがあります。

1. AWSリソースへのアクセス権限を持つIAMユーザーを作成し、Pod内でIAMユーザーのアクセスキー/シークレットアクセスキーを指定する。
2. Podが配置されているホスト(EC2)に対してIAMロールをアタッチし、STSにより一時的なアクセスキーを取得する。

いずれも、EC2上のプログラムからAWSリソースへアクセスする際に用いる仕組みを踏襲しています。
ただし、EC2上での仕組みがKubernetesにそのまま適用できる訳ではなく、Kubernetes特有の仕組みと組み合わせて行います。

1.においては、Pod内のプログラムへアクセスキー/シークレットアクセスキーを安全に渡すために、Kubernetesの「Secret」リソースを使います。

2.においては、Pod内のプログラムへ一時的なアクセスキーを適用するスタンダードな仕組みはまだありませんが、現在は「kube2iam」や「kiam」といったOSSが公開されています。

## 何故「kube2iam」や「kiam」のような仕組みが必要か？

Podが稼働するホスト(EC2)に対してIAMロールを介したアクセス権限を与えた場合、ホスト上で動作する全Podに一律のアクセス権を与えることになります。
それではセキュリティ的によろしくないので、Pod単位でIAMアクセスポリシーを割り当てる仕組みが考えられました。
Kubernetes(のアクセス管理機能)とIAMの間に入ってアクセス権限の管理を行ってくれるのが「kube2iam」や「kiam」です。


# kube2iamの利用手順

## 前提

ここでは、Amazon EKSユーザーガイドの「ご利用開始にあたって(Getting Started)」の手順に従って構築したEKSクラスタ環境を前提としています。
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/getting-started.html


## 1. IAMロールの準備

まず、kube2iamで必要となる2つのIAMロールを準備します。


### (1) Worker Node用のIAMロール

Getting Startedの手順でクラスタを構築した場合、Worker Node用のIAMロールとして「XXXXXXXX-NodeInstanceRole-YYYYYYYYYYYY」という名前のロールが既に作成されているはずです。
(XXXXXXXX：作成時に付けた任意の名前、YYYYYYYYYYYY：ランダムな英数字)

このロールに対して「任意のロールに対してAssumeRoleを行うことができる権限」を与えます。

付与するポリシーをJSONで記述：

```node-role-permission-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "*"
    }
  ]
}
```

記述したJSONを指定してポリシーを作成：

```Shell
aws iam create-policy \
  --policy-name sts-assumerole-policy \
  --policy-document file://node-role-permission-policy.json
```

ロールにポリシーをアタッチ：

```Shell
aws iam attach-role-policy \
  --role-name XXXXXXXX-NodeInstanceRole-YYYYYYYYYYYY \
  --policy-arn arn:aws:iam::123456789012:policy/sts-assumerole-policy
```


### (2) Pod用のIAMロール

Podに対してアクセス権限を設定するためのIAMロールを作成します。

IAMロールの信頼関係ポリシードキュメントとして、「EC2」および「Worker Node用のIAMロール」からのAssumeRoleを許可する設定を記述します。

```pod-role-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/XXXXXXXX-NodeInstanceRole-YYYYYYYYYYYY"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

記述した信頼関係ポリシードキュメントのJSONファイルを指定して、IAMロールを作成します。

```Shell
aws iam create-role \
  --role-name MyPodRole \
  --assume-role-policy-document file://pod-role-trust-policy.json
```

作成したロールに対して、具体的に付与したい「AWSリソースへのアクセス権限」を設定します。
例えば、AWS管理ポリシーの「S3読み取り専用」権限であれば、以下のようにします。

```Shell
aws iam attach-role-policy \
  --role-name MyPodRole \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
```


## 2. kube2iamのデプロイ準備

2つのマニフェスト(yaml)を記述します。
(わかりやすいように2つに分けただけなので、1つにまとめても問題ありません)

<dl>
<dt><code>kube2iam-sa.yaml</code></dt>
<dd>kube2iamがKubernetes上で動作するために必要な権限を設定します。<br>
作成するリソース： ServiceAccount、ClusterRole、ClusterRoleBinding</dd>
</dl>

<dl>
<dt><code>kube2iam-ds.yaml</code></dt>
<dd>kube2iamの処理を行う実体となるコンテナを各ホスト上に配置します。<br>
作成するリソース： DaemonSet</dd>
</dl>

```kube2iam-sa.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kube2iam
  namespace: kube-system
---
apiVersion: v1
items:
  - apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: kube2iam
    rules:
      - apiGroups: [""]
        resources: ["namespaces","pods"]
        verbs: ["get","watch","list"]
  - apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: kube2iam
    subjects:
    - kind: ServiceAccount
      name: kube2iam
      namespace: kube-system
    roleRef:
      kind: ClusterRole
      name: kube2iam
      apiGroup: rbac.authorization.k8s.io
kind: List
```

```kube2iam-ds.yaml
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube2iam
  namespace: kube-system
  labels:
    app: kube2iam
spec:
  selector:
    matchLabels:
      app: kube2iam
  template:
    metadata:
      labels:
        app: kube2iam
    spec:
      serviceAccountName: kube2iam
      hostNetwork: true
      containers:
        - name: kube2iam
          image: jtblin/kube2iam:latest
          imagePullPolicy: Always
          args:
            - "--auto-discover-base-arn"
            - "--iptables=true"
            - "--host-ip=$(HOST_IP)"
            - "--host-interface=eni+"
            - "--verbose"
          env:
            - name: HOST_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          ports:
            - containerPort: 8181
              hostPort: 8181
              name: http
          securityContext:
            privileged: true
```

コンテナ「kube2iam」のオプションパラメーター(```args:```)の意味は以下の通りです。

<dl>
<dt><code>--auto-discover-base-arn</code></dt>
<dd>文字通り、ロールのARNのベース部分(arn:aws:iam::123456789012:role/)を自動的に検出してくれる設定です。<br>
うまく検出してくれない場合は、明示的に<code>--base-role-arn=arn:aws:iam::123456789012:role/</code>と指定する必要があるかもしれません。</dd>
</dl>

<dl>
<dt><code>--iptables=true</code></dt>
<dd>このオプションで「true」を指定した場合、ホスト(EC2)のiptablesに必要な設定を自動的に登録します。<br>
この指定を行わないと、手動でiptablesの設定を行う必要があります。
</dd>
</dl>

<dl>
<dt><code>--host-ip=$(HOST_IP)</code></dt>
<dd>「--iptables」を指定する場合は「--host-ip」も必ず指定する必要があります。</dd>
</dl>

<dl>
<dt><code>--host-interface=eni+</code></dt>
<dd>「Getting Started」の手順に従ってEKS環境を構築した場合はPod Networkingとして「amazon-vpc-cni-k8s」が採用されるため、このオプションは「eni+」を指定する必要があります。
</dd>
</dl>

<dl>
<dt><code>--verbose</code></dt>
<dd>詳細なログを出力します。トラブルシューティング時に役立つでしょう。(役立ちました)
</dd>
</dl>


## 3. kube2iamのデプロイ

準備したマニフェストを使ってkube2iamをデプロイします。

```Shell
$ kubectl apply -f kube2iam-sa.yaml
serviceaccount "kube2iam" created
clusterrole.rbac.authorization.k8s.io "kube2iam" created
clusterrolebinding.rbac.authorization.k8s.io "kube2iam" created
```

```Shell
$ kubectl apply -f kube2iam-ds.yaml
daemonset.apps "kube2iam" created
```

DaemonSetが登録され、Podが正常に動作していることを確認します。

```Shell
$ kubectl get daemonsets -n kube-system
NAME         DESIRED   CURRENT   READY     UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
aws-node     3         3         3         3            3           <none>          5d
kube-proxy   3         3         3         3            3           <none>          5d
kube2iam     3         3         3         3            3           <none>          5m

$ kubectl get pods -n kube-system
NAME                       READY     STATUS    RESTARTS   AGE
aws-node-bmv7x             1/1       Running   0          5d
aws-node-lnrkb             1/1       Running   1          5d
aws-node-nkx8j             1/1       Running   0          5d
kube-dns-7cc87d595-7f4gk   3/3       Running   0          5d
kube-proxy-ffshl           1/1       Running   0          5d
kube-proxy-nbsww           1/1       Running   0          5d
kube-proxy-xq47h           1/1       Running   0          5d
kube2iam-2ffpd             1/1       Running   0          5m
kube2iam-2h5fd             1/1       Running   0          5m
kube2iam-wtlsz             1/1       Running   0          5m
```

Podのログを見てみます。

```Shell
$ kubectl logs -n kube-system kube2iam-2ffpd
time="2018-08-08T09:28:40Z" level=info msg="base ARN autodetected, arn:aws:iam::123456789012:role/"
time="2018-08-08T09:28:40Z" level=debug msg="Namespace OnAdd" ns.name=default
time="2018-08-08T09:28:40Z" level=debug msg="Namespace OnAdd" ns.name=kube-public
time="2018-08-08T09:28:40Z" level=debug msg="Namespace OnAdd" ns.name=kube-system
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnAdd" pod.iam.role= pod.name=aws-node-nkx8j pod.namespace=kube-system pod.status.ip=192.168.215.249 pod.status.phase=Running
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnAdd" pod.iam.role= pod.name=kube-proxy-ffshl pod.namespace=kube-system pod.status.ip=192.168.92.232 pod.status.phase=Running
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnAdd" pod.iam.role= pod.name=kube-proxy-nbsww pod.namespace=kube-system pod.status.ip=192.168.140.55 pod.status.phase=Running
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnAdd" pod.iam.role= pod.name=kube2iam-2ffpd pod.namespace=kube-system pod.status.ip=192.168.140.55 pod.status.phase=Pending
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnAdd" pod.iam.role= pod.name=kube-dns-7cc87d595-7f4gk pod.namespace=kube-system pod.status.ip=192.168.95.202 pod.status.phase=Running
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnAdd" pod.iam.role= pod.name=kube2iam-wtlsz pod.namespace=kube-system pod.status.ip=192.168.215.249 pod.status.phase=Pending
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnAdd" pod.iam.role= pod.name=aws-node-bmv7x pod.namespace=kube-system pod.status.ip=192.168.92.232 pod.status.phase=Running
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnAdd" pod.iam.role= pod.name=aws-node-lnrkb pod.namespace=kube-system pod.status.ip=192.168.140.55 pod.status.phase=Running
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnAdd" pod.iam.role= pod.name=kube2iam-2h5fd pod.namespace=kube-system pod.status.ip=192.168.92.232 pod.status.phase=Pending
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnAdd" pod.iam.role= pod.name=kube-proxy-xq47h pod.namespace=kube-system pod.status.ip=192.168.215.249 pod.status.phase=Running
time="2018-08-08T09:28:40Z" level=debug msg="Caches have been synced.  Proceeding with server."
time="2018-08-08T09:28:40Z" level=info msg="Listening on port 8181"
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnUpdate" pod.iam.role= pod.name=kube2iam-wtlsz pod.namespace=kube-system pod.status.ip=192.168.215.249 pod.status.phase=Running
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnUpdate" pod.iam.role= pod.name=kube2iam-2ffpd pod.namespace=kube-system pod.status.ip=192.168.140.55 pod.status.phase=Running
time="2018-08-08T09:28:40Z" level=debug msg="Pod OnUpdate" pod.iam.role= pod.name=kube2iam-2h5fd pod.namespace=kube-system pod.status.ip=192.168.92.232 pod.status.phase=Running
```

デフォルトのkube2iamポート「8181」で待ち受けを開始しているので、とりあえずOKのようです。


# 動作テスト

kube2iamを使用する設定を記述したPod(ReplicaSet)をデプロイして、AWSリソースへ実際にアクセスできることを確認します。

アノテーション```iam.amazonaws.com/role:```に、アクセス権限を設定したロールの名前(ここでは```MyPodRole```)を設定します。

```mypod-awscli.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: mypod-awscli
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mypod-awscli
  template:
    metadata:
      annotations:
        iam.amazonaws.com/role: MyPodRole
      labels:
        app: mypod-awscli
    spec:
      containers:
      - name: aws-cli
        image: fstab/aws-cli
        command: [ "/bin/bash", "-c", "--" ]
        args: [ "while true; do sleep 30; done;" ]
```

ReplicaSetをデプロイし、Podが起動したことを確認したら、Podに接続してbashを起動します。

```Shell
$ kubectl apply -f mypod-awscli.yaml 
replicaset.apps "mypod-awscli" created

$ kubectl get pods
NAME                 READY     STATUS    RESTARTS   AGE
mypod-awscli-94hgz   1/1       Running   0          52s

$ kubectl exec mypod-awscli-94hgz -it /bin/bash
(env) aws@mypod-awscli-94hgz:~$ 
```

バケットリストの参照を試み、参照できることを確認します。

```Shell
(env) aws@mypod-awscli-94hgz:~$ aws s3 ls
2018-08-08 09:55:48 mybucket20180808
```

削除を試みると、読み取り権限しか与えられていないため、失敗します。

```Shell
(env) aws@mypod-awscli-94hgz:~$ aws s3 rb s3://mybucket20180808
remove_bucket failed: s3://mybucket20180808/ An error occurred (AccessDenied) when calling the DeleteBucket operation: Access Denied
```


# 検証中に確認したエラー (失敗の記録)


## その1：「ServiceAccount」を設定しなかった場合

ドキュメントによっては、「ServiceAccount」を設定せずにDaemonSetのデプロイのみの手順で説明されているものがあります。
そのようにすると、kube2iamのPodログに以下のようなエラーが記録され続けて、正しく動作しません。

```Shell
$ kubectl logs kube2iam-mqqv6 -n kube-system
time="2018-08-13T18:13:00Z" level=info msg="base ARN autodetected, arn:aws:iam::123456789012:role/"
E0813 18:13:00.423621       1 reflector.go:199] github.com/jtblin/kube2iam/vendor/k8s.io/client-go/tools/cache/reflector.go:94: Failed to list *v1.Namespace: namespaces is forbidden: User "system:serviceaccount:kube-system:default" cannot list namespaces at the cluster scope
E0813 18:13:00.424728       1 reflector.go:199] github.com/jtblin/kube2iam/vendor/k8s.io/client-go/tools/cache/reflector.go:94: Failed to list *v1.Pod: pods is forbidden: User "system:serviceaccount:kube-system:default" cannot list pods at the cluster scope
E0813 18:13:01.426627       1 reflector.go:199] github.com/jtblin/kube2iam/vendor/k8s.io/client-go/tools/cache/reflector.go:94: Failed to list *v1.Namespace: namespaces is forbidden: User "system:serviceaccount:kube-system:default" cannot list namespaces at the cluster scope
E0813 18:13:01.427648       1 reflector.go:199] github.com/jtblin/kube2iam/vendor/k8s.io/client-go/tools/cache/reflector.go:94: Failed to list *v1.Pod: pods is forbidden: User "system:serviceaccount:kube-system:default" cannot list pods at the cluster scope
E0813 18:13:02.428618       1 reflector.go:199] github.com/jtblin/kube2iam/vendor/k8s.io/client-go/tools/cache/reflector.go:94: Failed to list *v1.Namespace: namespaces is forbidden: User "system:serviceaccount:kube-system:default" cannot list namespaces at the cluster scope
E0813 18:13:02.429634       1 reflector.go:199] github.com/jtblin/kube2iam/vendor/k8s.io/client-go/tools/cache/reflector.go:94: Failed to list *v1.Pod: pods is forbidden: User "system:serviceaccount:kube-system:default" cannot list pods at the cluster scope
```

EKS環境ではRBACが使われているため、ServiceAccountの設定は必須であるようです。


## その2：「--host-interface=eni+」を指定しなかった場合

kube2iamは、iptablesの設定によってPod・ホスト(EC2)間の通信をフックし、EC2インスタンスメタデータを書き換えることでアクセス制御を行います。
「--host-interface」オプションを省略した場合は「docker0」がデフォルト値で使われますが、その場合のiptablesの状態は以下のようになります。

```Shell
[ec2-user@ip-192-168-64-19 ~]$ sudo iptables -L -t nat --verbose --line-numbers
Chain PREROUTING (policy ACCEPT 1 packets, 60 bytes)
num   pkts bytes target         prot opt in      out     source               destination         
1        3   180 KUBE-SERVICES  all  --  any     any     anywhere             anywhere             /* kubernetes service portals */
2        1    60 DOCKER         all  --  any     any     anywhere             anywhere             ADDRTYPE match dst-type LOCAL
3        0     0 DNAT           tcp  --  docker0 any     anywhere             instance-data.us-west-2.compute.internal  tcp dpt:http to:192.168.64.19:8181
```

3番目の「doker0」に対する設定がありますが、「pkts」「bytes」の値がいずれも「0」であることに注目して下さい。(つまり、ここをパケットが全く通っていない＝Pod・ホスト間の通信に使われていない)

一方、「-host-interface=eni+」を指定した場合のiptablesは以下のようになります。

```Shell
[ec2-user@ip-192-168-92-232 ~]$ sudo iptables -L -t nat --verbose --line-numbers
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target         prot opt in     out     source               destination         
1     262K   20M KUBE-SERVICES  all  --  any    any     anywhere             anywhere             /* kubernetes service portals */
2       34  2040 DOCKER         all  --  any    any     anywhere             anywhere             ADDRTYPE match dst-type LOCAL
3     1028 61680 DNAT           tcp  --  eni+   any     anywhere             instance-data.us-west-2.compute.internal  tcp dpt:http to:192.168.92.232:8181
```

こちらは、ちゃんとパケットが通っていることが分かります。


# おわりに (「kiam」について)

とりあえず「EKSでkube2iamが動かせた」というレベルなので、これからまだ確認していかなければならないことが多々あると思います。

一方「kiam」についてですが、EKSで動かしてみようとしましたが、上手く行っていません。
kiamはモジュールが「server」と「agent」に分かれており、「server」モジュールはKubernetesのMaster Nodeで動かす前提になっています。
ところが、EKSはMaster NodeがマネージドなのでPodを配置できません。
無理やりWorker Nodeに配置するようにマニフェストを書き換えてみましたが、いろんなエラーで動かない状況です。

そもそも、kiamのアーキテクチャ的に、マネージドなMaster Nodeを持つEKSに適合するものなのかどうか、そこすら良く分かっていません。

何か分かったら、また記事にしたいと思います。