今月AWSでKubernetesでクラスター構築してる勢に衝撃が走りました。
EKS で Kubernetes サービスアカウントに IAM アクセス許可を割り当てることが出来るようになったのです!!
aws.amazon.com
これによって pod に対して IAM Role を割り当てやすくなりました。
今までは何もしない場合は EC2 に対して割り当てる IAM Role のポリシーを使うことになるので、クラスター内で異なる権限が必要なアプリケーションを運用している場合はそれらを合わせたポリシーを適用する必要があり、God IAM Role を作成していました。
自分の場合はそれを解決する手段として 3rd party 製の kube2iam を使用していました。
今回 AWS 側でそれをサポートしてくれるようになったので、自分は必要な IAM Role と サービスアカウントを紐づけるだけで良いです。
ただタイトルには EKS と書いてあったため、kops で構築した自分のクラスターは諦めていたのですが、↓のエントリをみて OSS として公開されていることを知り今回試してみました。
全体の流れ
まず全体の流れですが下記のウォークスルーを一通りやることをオススメします。
eksctl
を使ってコマンドをコピペしていくだけで EKS を使って全体的な流れを把握できます。
それを踏まえ今回やった大まかな流れとしては次の通りです。
- ゴールの確認
- OIDC ID プロバイダー作成
- amazon-eks-pod-identity-webhook のpod を作成する
- サービスアカウントに IAM Role を付与して実行
1. ゴールの確認
今回はウォークスルーでも紹介されていた demo app を使います。
中身はシンプルでプログラムを実行すると指定した S3 のバケットにファイルをアップロードするだけです。
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 に書かれています。
あとは 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 を作成したので、そちらを確認ください。
なので現時点では自分でビルドしたものを配置するしか複数の 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 から取得することができるので↓を参考にして取得します。
これで 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 の信頼関係の情報を修正します。
複数のサービスアカウントから参照したい場合は✳︎が使えるのでそれを使うと良さそうです。
open /var/run/secrets/eks.amazonaws.com/serviceaccount/token: permission denied
IAM Role を付与したサービスアカウントを付与したマニフェストを適用した際にこのエラーが起きることがあります。
これは root で起動している pod なら問題ないのですが、それ以外のユーザーで起動している時に起きるようです。
解決策としては現状は securityContext
を指定します。
↓は external-dns のPRですが自分で作成したアプリケーションでも動作することを確認しました。
github.com
Pod に AWS_ROLE_ARN
や AWS_WEB_IDENTITY_TOKEN_FILE
などの IAM 情報が渡されない
これにはいくつかの要因があると思います。
僕の場合は MutatingWebhookConfiguration
の caBundle の値が間違っており下記を参考に生成したものをセットしたら無事付与されました。
secret_name=$(kubectl get sa default -o jsonpath='{.secrets[0].name}') export CA_BUNDLE=$(kubectl get secret/$secret_name -o jsonpath='{.data.ca\.crt}' | tr -d '\n')
最後に
今回は AWS 上に kops で構築した Kubernetes のクラスターで IAM Roles for Service Accounts (IRSA) を使用する手順を書きました。
間違っていたりここはこうしたほうが良いなどありましたがぜひ twitter: @hatappi でお知らせいただけると嬉しいです。