kube2iamからkiamに乗り換えた

AWS上に構築したKubernetesクラスタ内のPodが,AWSのリソースにアクセスしようとしたときには,もちろんAWS IAM Roleか,AWSの認証情報を使ってAWSAPIを叩く必要がある.

しかし,Kubernetesクラスタだ.もちろんノードはいっぱいあるし,その上では様々なPodが動くことになる.そういうときに,ノード全体に強い権限を持ったIAM Roleを付与するのは,あまり嬉しくない. 意図せず別のPodが,アクセスしてほしくないリソースにアクセスできることになってしまう.

というわけで,Podごとに個別にIAM Roleを付与したいという要求があるのだが,それを満たすのがkube2iamkiamというOSSだ.

で,中身の仕組みの話を詳しくここではしないのだが,基本的にkube2iamの方が仕組みが単純である. ただし,たまにバグを拾うことがあって,正しくAWSのCredentialを引けない場合があった.

というわけで今回kiamに移行した.

ちなみにKubernetesクラスタはkopsで構築している. EKSを使っている場合は,つい最近発表されたIAM Role for Service Account を使うと良いと思うよ.

前提条件

kiamは,serverとagentの2つのプロセスに分かれているのだが,このうちserver側のプロセスはkubernetesクラスタのmasterノードで動作することを前提にしている. そのためmasterノードにpodを配置できない,EKSのような環境の場合,kiamは選択できないことになる.

今回は,kopsで構築したクラスタなので,問題なくmasterノードに配置することができる.

また,kube2iamはhelmでインストールしている.これをアンインストールし,kiamもhelmでインストールすることにする. ただし,一度kube2iamをアンインストールしてしまうので,一時的にIAMの認証が通らない時間が発生してしまうが,これは許容している.

kube2iamの削除

実際に移行する際は,kiamのインストール直前でやることをおすすめする.

$ helm delete --purge kube2iam

Podはこれで全て消える.

iptablesを削除する

一応iptablesを編集するような設定を書いていた場合,それを削除したほうが良い.

workerのnodeにsshで入り,

$ sudo iptables -t nat -n -L PREROUTING --line-numbers
Chain PREROUTING (policy ACCEPT)
num target prot opt source destination
1 cali-PREROUTING all -- 0.0.0.0/0 0.0.0.0/0 /* cali:6gwbT8clXdHdC1b1 */
2 KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
3 DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
4 DNAT tcp -- 0.0.0.0/0 169.254.169.254 tcp dpt:80 to:10.192.102.212:8181
5 DNAT tcp -- 0.0.0.0/0 169.254.169.254 tcp dpt:80 to:10.192.102.212:8881

のようにしてiptablesを確認する.

この場合,8181 ポートを使っているルールがkube2iamなので,これを削除する.

$ sudo iptables -t nat -D PREROUTING 4

medium.com

これでkube2iamの削除は完了である.

kiamを入れる

kiamが使うIAM Roleを準備する

必要なRoleについては,

github.com

に記述されている.

k8sクラスタのmaster nodeに付与するRole

master nodeのIAM Roleは既に用意されていることが多い.kopsだと自動作成されるしね. なので,新規作成ではなく修正になる可能性が高いが,以下のようなIAM Roleを作成する.

resource "aws_iam_role" "k8s_master_role" {
  name               = "k8s-master-role"
  path               = "/"
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

そして,このRoleはEC2インスタンスに付与されるので,Instance Profileになっている必要がある.

resource "aws_iam_instance_profile" "k8s_master_profile" {
  name = "k8s-master-profile"
  role = aws_iam_role.k8s_master_role.name
}

kiamのserverに付与するRole

masterインスタンス内で動くkiam serverプロセスに,Roleを渡す必要がある. こいつは,先程作成したk8sのmaster roleにsts:Assume できる必要がある.

resource "aws_iam_role" "kiam_master_role" {
  name               = "kiam-master-role"
  path               = "/"
  assume_role_policy = data.template_file.k8s_master_assume_role_policy.rendered
}

data "template_file" "k8s_master_assume_role_policy" {
  template = file(
    "${path.module}/aws_iam_role_policies/arn_assume_role_policy.json.tpl"
  )

  vars = {
    arn = aws_iam_role.k8s_master_role.arn
  }
}

arn_assume_role_policy.json.tpl の中身がこちら.

{
  "Version":"2012-10-17",
  "Statement": [
    {
      "Effect":"Allow",
      "Principal": {
        "AWS": "${arn}"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

k8sのmaster roleに付与するpolicy

kubernetesのmasterノードに付与されるIAM Roleには,以下のようなpolicyをくっつける.

resource "aws_iam_policy_attachment" "sts_assume_for_kiam_role" {
  name = "sts-assume-role-for-kiam-role"

  roles = [
    aws_iam_role.k8s_master_role.name
  ]

  policy_arn = aws_iam_policy.sts_assume_for_kiam_role_policy.arn
}


resource "aws_iam_policy" "sts_assume_for_kiam_role_policy" {
  name        = "sts-assume-for-kiam-role-policy"
  path        = "/"
  description = ""
  policy      = data.template_file.sts_assume_for_kiam_role_policy.rendered
}

data "template_file" "sts_assume_for_kiam_role_policy" {
  template = file(
    "${path.module}/aws_iam_policies/sts_assume_role_for_role_policy.json.tpl"
  )

  vars = {
    role = aws_iam_role.kiam_master_role.arn
  }
}

sts_assume_role_for_role_policy.json.tpl はこちら.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "sts:AssumeRole"
      ],
      "Resource": "${role}"
    }
  ]
}

kiamのserverプロセスに付与するPolicy

kiamのserverプロセスが利用するIAM Roleには以下のようなpolicyを付与する.

resource "aws_iam_policy_attachment" "sts_assume_role_for_all" {
  name = "sts-assume-role-for-all"

  roles = [
    aws_iam_role.kiam_master_role.name
  ]

  policy_arn = aws_iam_policy.sts_assume_role_for_all_policy.arn
}

resource "aws_iam_policy" "sts_assume_role_for_all_policy" {
  name        = "sts-assume-role-for-all-policy"
  path        = "/"
  description = ""
  policy      =<<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "*"
    }
  ]
}
EOF
}

これで準備はできた.

kaimのhelm chartのオプションについて

chartはここにあるものを使う.

github.com

なお,オプションの指定は全てhelmfile の形式で記述している.

hostの設定

まず,kube2iamと同じくnodeのiptablesを追記する必要がある.そのため,

- agent:
    host:
      iptables: true
      interface: "!eth0"

と指定する.

俺が使っているkopsのkubernetesクラスタはnetworking modeとしてamazon-vpc-routed-eniを利用している.そのため !eth0 を指定しているが,ここは使っているクラスタのnetworking modeに合わせたものを設定すると良い. ちなみにkiam側でサポートしている値は以下の通り.

github.com

ssl-certsのボリュームをマウントする

kiamのhelm chartはssl-certsのボリュームマウントが定義されていない.しかし,

github.com

この通り,ホスト側のcertsをマウントする必要があり,それを追記しなければならない.

kopsはubuntuをベースのノードとしているため, /etc/ssl/certs を指定すれば良い.

なお,この設定はserverとagent両方に入れる必要がある.

- agent:
    extraHostPathMounts:
      - name: ssl-certs
        hostPath: /etc/ssl/certs
        mountPath: /etc/ssl/certs
        readOnly: true
- server:
    extraHostPathMounts:
      - name: ssl-certs
        hostPath: /etc/ssl/certs
        mountPath: /etc/ssl/certs
        readOnly: true

tolerationsを付与する

github.com

の通りに,こちらも指定する必要がある.

- server:
    tolerations:
      - key: "node-role.kubernetes.io/master"
        effect: "NoSchedule"
        operator: "Exists"

nodeSelectorを入れる

kiamは,agentとserverに分かれているという話をしたが,agentはnodeで,serverはmasterで動くことになる. これが,serverをnodeで動かしたりするとエラーになるため,serverに関しては実行するnodeをmasterに限定する必要がある.

そのためにこのような制約を入れる.

- server:
    nodeSelector:
      kubernetes.io/role: master

assumeRoleArnを指定する

先程作成した,kiamのserverが利用するIAM Roleをkiam serverのプロセスに教えてやる必要がある.これは自動探索されたりはしない.

- server:
    assumeRoleArn: arn:aws:iam::123456789:role/kiam-master-role

これで,あとは helmfile sync すればこの通りのkiamがインストールされる.

PodにIAM Roleを付与する

IAM Roleの準備

各Podが使うIAM Roleを作る.

resource "aws_iam_role" "fascia_prd_pod_role" {
  name               = "fascia-prd-pod-role"
  path               = "/"
  assume_role_policy = data.template_file.k8s_pod_assume_role_policy.rendered
}


data "template_file" "k8s_pod_assume_role_policy" {
  template = file(
    "${path.module}/aws_iam_role_policies/k8s_pod_assume_role_policy.json.tpl",
  )

  vars = {
    kiam_master_role_arn = aws_iam_role.kiam_master_role.arn
  }
}

k8s_pod_assume_role_policy.json.tpl はこちら.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    },
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "${kiam_master_role_arn}"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Podにannotationを追加

ここで追加するannotationはkube2iamで使っていたものとまったく同じである.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fascia-deployment
  labels:
    app: fascia
spec:
  replicas: 2
  template:
    metadata:
      annotations:
        iam.amazonaws.com/role: fascia-prd-pod-role

Namespaceにannotationを追加

ここはkube2iamと違って,namespaceにもannotationを追加する必要がある. namespaceのannotationでは,namespace内で使えるIAM Roleを正規表現で絞ることができる.

ここで下手な指定をすると後でエラーになるのだが,これについては後述.

apiVersion: v1
kind: Namespace
metadata:
  name: my-namespace
  labels:
    name: my-namespace
  annotations:
    iam.amazonaws.com/permitted: ".*"

出くわしたエラーたち

Kubernetesのmaster node,もしくはkiam server processのIAM Roleが正しく設定されていない場合

そもそもいかなるRoleを引くこともできないので,kiam serverを起動した段階で,

{
  "level":"info",
  "msg":"started namespace cache controller",
  "time":"2019-09-07T13:39:47Z"
}
{
  "level":"error",
  "msg":"error requesting credentials: AccessDenied: Access denied\n\tstatus code: 403, request id: f5ad7de6-d174-11e9-9914-65d5429e551b",
  "pod.iam.role":"fascia-prd-pod-role",
  "time":"2019-09-07T13:39:48Z"
}
{
  "generation.metadata":0,
  "level":"error",
  "msg":"error warming credentials: AccessDenied: Access denied\n\tstatus code: 403, request id: f5ad7de6-d174-11e9-9914-65d5429e551b",
  "pod.iam.role":"fascia-prd-pod-role",
  "pod.name":"fascia-deployment-d64b565c-psbvk",
  "pod.namespace":"web-public",
  "pod.status.ip":"10.0.12.70",
  "pod.status.phase":"Running",
  "resource.version":"36307952",
  "time":"2019-09-07T13:39:48Z"
}

このような出力が出る. これが出ている場合は,そもそもIAM Roleを正しく取得できていないので,以下の点を確認する.

  1. kiamのdocの通りのIAMが設定されているかどうか
  2. 上のIAM内の,kiam-server (※server_nodeではないので注意) が,kiam server起動時の --assume-role-arn に渡っているかどうか.

を確認すると良い. だいたいこの2つが合致した時点で,先のエラーが消え,

{
  "credentials.access.key":"xxxx",
  "credentials.expiration":"2019-09-07T14:28:23Z",
  "credentials.role":"fascia-prd-pod-role",
  "level":"info",
  "msg":"requested new credentials",
  "time":"2019-09-07T14:13:23Z"
}
{
  "credentials.access.key":"xxxx",
  "credentials.expiration":"2019-09-07T14:28:23Z",
  "credentials.role":"fascia-prd-pod-role",
  "generation.metadata":0,
  "level":"info",
  "msg":"fetched credentials",
  "pod.iam.role":"fascia-prd-pod-role",
  "pod.name":"fascia-deployment-d64b565c-psbvk",
  "pod.namespace":"web-public",
  "pod.status.ip":"10.0.12.70",
  "pod.status.phase":"Running",
  "resource.version":"36307952",
  "time":"2019-09-07T14:13:23Z"
}

このようなメッセージに変更される.

annotationが正しく設定されていない場合

これはハマった. kiamは,podのannotation自体はkube2iamとまったく同じではあるのだが, namespace側でもannotationを追記する必要がある.

ちなみにこれを見落とし,podにのみannotationを付けていた場合,

{
  "generation.metadata":0,
  "level":"error",
  "msg":"pod denied by policy",
  "pod.iam.requestedRole":"whalebirdorg-prd-pod-role",
  "pod.iam.role":"whalebirdorg-prd-pod-role",
  "pod.name":"whalebirdorg-deployment-5b94dc6d94-9b4ms",
  "pod.namespace":"web-public",
  "pod.status.ip":"10.0.42.68",
  "pod.status.phase":"Running",
  "policy.explanation":"namespace policy expression (empty) forbids role whalebirdorg-prd-pod-role",
  "resource.version":"36299056",
  "time":"2019-09-07T14:50:51Z"
}
 

このようなエラーが出続けることになった.

namespaceには先に示したように

apiVersion: v1
kind: Namespace
metadata:
  name: my-namespace
  labels:
    name: my-namespace
  annotations:
    iam.amazonaws.com/permitted: ".*"

このようなannotationをつけることで,無事認証が通るようになる.

ちなみに,ここで指定しているのは正規表現である.間違った正規表現を指定すると,

 {
  "generation.metadata":0,
  "level":"error",
  "msg":"error checking policy: error parsing regexp: missing argument to repetition operator: `*`",
  "pod.iam.requestedRole":"whalebirdorg-prd-pod-role",
  "pod.iam.role":"whalebirdorg-prd-pod-role",
  "pod.name":"whalebirdorg-deployment-5b94dc6d94-9b4ms",
  "pod.namespace":"web-public",
  "pod.status.ip":"10.0.42.68",
  "pod.status.phase":"Running",
  "resource.version":"36299056",
  "time":"2019-09-07T14:55:52Z"
}

このようにしっかり怒られることになる.

そして,例えばIAM認証を使うPodがkube-system に立ち上がる場合,もちろん既存のnamespaceであるkube-systemにもnamespaceを付与する必要がある.

apiVersion: v1
kind: Namespace
metadata:
  name: kube-system
  labels:
    name: kube-system
  annotations:
    iam.amazonaws.com/permitted: ".*"

まとめ

kube2iamよりは安定して動作している.

ただし,kube2iamにしろkiamにしろ,daemonsetでpodを動かして,IAMへの認証をそちらに捻じ曲げて処理しているに過ぎない.意図せずdaemonsetのpodが死んだり,疎通できなくなった場合には,もちろん認証が通らなくなる. この点では,やはり将来的にはIRSAに移行したほうが安全なのではないだろうか.