kopsでaws上に作成したKubernetesクラスターでIAM Roles for Service Accountsを使う

今月AWSKubernetesクラスター構築してる勢に衝撃が走りました。
EKS で Kubernetes サービスアカウントに IAM アクセス許可を割り当てることが出来るようになったのです!!
aws.amazon.com

これによって pod に対して IAM Role を割り当てやすくなりました。
今までは何もしない場合は EC2 に対して割り当てる IAM Role のポリシーを使うことになるので、クラスター内で異なる権限が必要なアプリケーションを運用している場合はそれらを合わせたポリシーを適用する必要があり、God IAM Role を作成していました。

自分の場合はそれを解決する手段として 3rd party 製の kube2iam を使用していました。
今回 AWS 側でそれをサポートしてくれるようになったので、自分は必要な IAM Role と サービスアカウントを紐づけるだけで良いです。

ただタイトルには EKS と書いてあったため、kops で構築した自分のクラスターは諦めていたのですが、↓のエントリをみて OSS として公開されていることを知り今回試してみました。

aws.amazon.com

全体の流れ

まず全体の流れですが下記のウォークスルーを一通りやることをオススメします。
eksctl を使ってコマンドをコピペしていくだけで EKS を使って全体的な流れを把握できます。

aws.amazon.com

それを踏まえ今回やった大まかな流れとしては次の通りです。

  1. ゴールの確認
  2. OIDC ID プロバイダー作成
  3. amazon-eks-pod-identity-webhook のpod を作成する
  4. サービスアカウントに IAM Role を付与して実行

1. ゴールの確認

今回はウォークスルーでも紹介されていた demo app を使います。
中身はシンプルでプログラムを実行すると指定した S3 のバケットにファイルをアップロードするだけです。

github.com

README.md に実行手順は書いてあるので、それにしたがって実行してみます。
まだ何もしてないので失敗するはずです。

$ kubectl create sa s3-echoer
$ sed -e "s/TARGET_BUCKET/${TARGET_BUCKET}/g" s3-echoer-job.yaml.template > s3-echoer-job.yaml
$ kubectl apply -f s3-echoer-job.yaml

すると実行結果はエラーになって kubectl logs とかで見ると次のようなエラーになるはずです。

2019/09/23 10:03:51 Can't upload to S3: NoCredentialProviders: no valid providers in chain. Deprecated.
        For verbose messaging see aws.Config.CredentialsChainVerboseErrors

今回はこれを成功できるようにします。

2. OIDC ID プロバイダーの作成

今回の中で一番面倒です。
EKS なら eksctl を使うと数回コマンドを叩くだけで OIDC ID プロバイダーができて便利なのですが、 EKS を使わない場合は自分であれこれ作成する必要があります。

まずは issuer を作成します。
これに関してはドキュメントがあるのでこれに沿ってやっていけば良いです。
https://github.com/aws/amazon-eks-pod-identity-webhook/blob/master/SELF_HOSTED_SETUP.md
ただ自分は terraform を使ったり kops 使って構築したりしていて少し手順が違うのでそれを書いていきます。

鍵を作成

$ PRIV_KEY="sa-signer.key"
$ PUB_KEY="sa-signer.key.pub"
$ PKCS_KEY="sa-signer-pkcs8.pub"
$ ssh-keygen -t rsa -b 2048 -f $PRIV_KEY -m pem
$ ssh-keygen -e -m PKCS8 -f $PUB_KEY > $PKCS_KEY

S3 からファイルを公開する

まず S3 のバケットを terraform で作成します。

resource "random_uuid" "oidc-s3" {
}

resource "aws_s3_bucket" "oidc" {
  bucket = "oidc-${random_uuid.oidc-s3.result}"
  acl    = "private"

  versioning {
    enabled = true
  }
}

ISSUER_HOSTPATH は terraform だと output で S3 から取り出すことができます。

output "oidc_website_endpoint" {
  value = "${aws_s3_bucket.oidc.bucket_domain_name}"
}
$ ISSUER_HOSTPATH=$(terraform output -json | jq -r ".oidc_website_endpoint.value")
$ echo ${ISSUER_HOSTPATH}
# oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com

ドキュメントにも書かれているようにこれで OIDC discovery の json が作成できます。

cat <<EOF > discovery.json
{
    "issuer": "https://$ISSUER_HOSTPATH/",
    "jwks_uri": "https://$ISSUER_HOSTPATH/keys.json",
    "authorization_endpoint": "urn:kubernetes:programmatic_authorization",
    "response_types_supported": [
        "id_token"
    ],
    "subject_types_supported": [
        "public"
    ],
    "id_token_signing_alg_values_supported": [
        "RS256"
    ],
    "claims_supported": [
        "sub",
        "iss"
    ]
}
EOF

次は jwks_uri に使う key.json を作成します。
これはコマンド一発です。

$ git clone https://github.com/aws/amazon-eks-pod-identity-webhook
$ cd amazon-eks-pod-identity-webhook
$ go run ./hack/self-hosted/main.go -key $PKCS_KEY  | jq '.keys += [.keys[0]] | .keys[1].kid = ""' > keys.json

補足情報として生成される key.json は kid が空のものとありのものがありますが、これになった背景は issue に書かれています。

github.com

あとは S3 にアップロードして完了です。

$ aws s3 cp --acl public-read ./discovery.json s3://$S3_BUCKET/.well-known/openid-configuration
$ aws s3 cp --acl public-read ./keys.json s3://$S3_BUCKET/keys.json

念のため curl とかでリクエストできるか確認しておくと良いです。

$ curl ${ISSUER_HOSTPATH}/.well-known/openid-configuration
$ curl ${ISSUER_HOSTPATH}/keys.json

次は kops で apiserver にドキュメントにも書いてある必要なパラメータを設定していきます。
https://github.com/aws/amazon-eks-pod-identity-webhook/blob/master/SELF_HOSTED_SETUP.md#kubernetes-api-server-configuration

最初の手順で行った公開鍵(pkcs8) と秘密鍵を apiserver のある master インスタンスに配置する必要があります。
これは kops がサポートしています。
それが fileAssets という機能です。

この機能を使ってファイルを kops 経由で配置します。
自分の場合は BASE64エンコードした鍵を配置しました。
ここで一つ注意が必要なのですが、 fileAssets がサーバーに配置する際に BASE64 を decode するのですが、その際に base64 で encode した時につく末尾の = を取り除く必要があります。
コードは↓です。 これで少しはまりました。
https://github.com/kubernetes/kops/blob/master/nodeup/pkg/model/file_assets.go#L91

spec:
  fileAssets:
    - name: service-account-signing-key-file
      path: /srv/kubernetes/assets/service-account-signing-key
      isBase64: true
      content: {{.service_account_private_key}}
    - name: service-account-key-file
      path: /srv/kubernetes/assets/service-account-key
      isBase64: true
      content: {{.service_account_key}}

apiServerの設定はこんな感じになります。

spec:
  kubeAPIServer:
    serviceAccountKeyFile:
      - "/srv/kubernetes/server.key"
      - "/srv/kubernetes/assets/service-account-key"
    serviceAccountSigningKeyFile: /srv/kubernetes/assets/service-account-signing-key
    apiAudiences:
      - oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com
    serviceAccountIssuer: https://oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com

ここで一つ注意が必要なのですが、 kops 1.13.0 では serviceAccountKeyFile を複数していするとエラーになります。
詳細は issue を作成したので、そちらを確認ください。

github.com

なので現時点では自分でビルドしたものを配置するしか複数の key を指定することはできないようです。

これで issuer ( https://$ISSUER_HOSTPATH ) の準備ができました。

次は IAM ID プロパイダー でOIDC プロバイダーを作成します。

これは terraform で作成します。

resource "aws_iam_openid_connect_provider" "iam-role-sa" {
  url = "https://oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com"

  client_id_list = [
    "oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com"
  ]

  thumbprint_list = [
    "xxxxxxxx"
  ]
}

thumbprint は issuer url から取得することができるので↓を参考にして取得します。

docs.aws.amazon.com

これで OIDC プロバイダーを作成できました。
EKS で eksctl 使えばすぐだったけど自分でやると結構手間だった。
マネージメント最高!

3. amazon-eks-pod-identity-webhook のpod を作成する

manifest file は aws/amazon-eks-pod-identity-webhook に用意されているのでこちらを今回は使用します。

docker image の用意

公式では提供されてないので自分でビルドします。

$ export REGION=ap-northeast-1
# AWSのアカウントID
$ export REGISTRY_ID=1111111
$ export IMAGE_NAME=eks/pod-identity-webhook
$ aws ecr create-repository --repository-name ${IMAGE_NAME}
$ make push

待ちます。

manifest の apply

あとはドキュメントに書かれている↓のコマンドを実行するだけなのですが、その前に一つだけ修正をします。
起動時の --token-audience=sts.amazonaws.com から ISSUER_HOSTPATH に変更します。

$ make cluster-up IMAGE=${IMAGE_NAME}

用意されている Makefile の target は namespace: default をベースにして作成されているので他の namespace で使いたい時は変更します。

これで Kubernetes 側の準備はできました。

4. サービスアカウントに IAM Role を付与して実行

では最後に最初にエラーになった s3-echoer でサービスアカウントに IAM Role を付与して実行しましょう。

まずは Role を作成します。
Role 作成時の Principal が重要でさきほど作成した OIDC プロバイダーの ARN を指定します。

resource "aws_iam_role" "s3-echoer" {
  name = "s3-echoer"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "${aws_iam_openid_connect_provider.iam-role-sa.arn}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com:sub": "system:serviceaccount:default:s3-echoer"
        }
      }
    }
  ]
}
EOF
}

resource "aws_iam_policy" "s3-echoer" {
  name        = "s3-echoer"
  path        = "/"
  description = "s3-echoer policy"

  policy = <<EOF
{
 "Version": "2012-10-17",
 "Statement": [
   {
     "Effect": "Allow",
     "Action": [
       "s3:*"
     ],
     "Resource": [
       "*"
     ]
   }
 ]
}
EOF
}


resource "aws_iam_role_policy_attachment" "s3-echoer" {
  role       = "${aws_iam_role.s3-echoer.name}"
  policy_arn = "${aws_iam_policy.s3-echoer.arn}"
}

あとは 先ほど作成したサービスアカウントの annotation に Role を付与します。

$ kubectl annotate sa s3-echoer eks.amazonaws.com/role-arn=${さっき作ったRoleのARN}

これで再度 job を実行すれば次は成功するはずです。

Troubleshooting

job 実行実行時に認証時に Access Denied と表示される

自分がこのエラーに遭遇した時は サービスアカウントに指定した IAM Role の信頼関係で条件に一致していませんでした。

つまり次のように指定していたにも関わらず default namespace の app というサービスアカウントに付与していた場合などです。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      ....
      "Condition": {
        "StringEquals": {
          "oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com:sub": "system:serviceaccount:default:s3-echoer"
        }
      }
    }
  ]
}

解決方法としては IAM Role の信頼関係の情報を修正します。
複数のサービスアカウントから参照したい場合は✳︎が使えるのでそれを使うと良さそうです。

docs.aws.amazon.com

open /var/run/secrets/eks.amazonaws.com/serviceaccount/token: permission denied

IAM Role を付与したサービスアカウントを付与したマニフェストを適用した際にこのエラーが起きることがあります。
これは root で起動している pod なら問題ないのですが、それ以外のユーザーで起動している時に起きるようです。
解決策としては現状は securityContext を指定します。
↓は external-dns のPRですが自分で作成したアプリケーションでも動作することを確認しました。
github.com

最後に

今回は AWS 上に kops で構築した Kubernetesクラスターで IAM Roles for Service Accounts (IRSA) を使用する手順を書きました。

間違っていたりここはこうしたほうが良いなどありましたがぜひ twitter: @hatappi でお知らせいただけると嬉しいです。