この記事は LAPRAS Advent Calendar 2019 の4日目です.
AWS上にkubernetesクラスタを構築していると,Pod内のコンテナから,AWSのAPIを叩きたいことがある. そのためにはもちろんAWSの認証を通す必要があり,AWSのAccess Tokenとか,IAM Roleを使って認証を通すことになる.
一般的に,productionで運用するEC2の中に,IAM UserのAccess Tokenを置くことは推奨されず,大抵の場合はIAM Roleで解決すると思う.もちろん,kubernetesクラスタであっても,ノードはただのEC2なのでInstance Profileを使えばIAM Roleでの認証は可能になる. しかし,kubernetesはクラスタだ.もちろんノードはいっぱいあり,その上で様々なPodが動いている.ということは,ノードのインスタンスからIAM Roleでの認証をする場合,そのRoleは,クラスタ内で動くすべてのPodが要求する権限を保有する必要がある.これはかなり強大な権限を持つRoleになってしまう. おまけに,この状態だと,実はセキュリティ的にAWSへのアクセスを許可しないPodであっても,容易にAWSへの認証を通ってしまうことになる.
こういう状態を避けるために,kube2iamやkiamといったOSSが開発されてきた.
で,このkube2iamに結構なバグがあったため,kopsで構築していたkubernetesクラスタに関しては,kiamに載せ替えたことがあった.
しかし,kiamはmasterノードにPodを配置する必要があり,masterノードに手を入れられないEKSでは,kiamを使うことができなかった.
これを完全に解決する方法として,今年9月にIAM Role for Service Accountが発表された.
というわけで,EKS運用しているクラスタのIAM認証を,このIRSAに載せ替えた.
IRSAの仕組みについては,上記記事を参照してほしい.ここでは詳しい動作原理については説明しない.
載せ替え手順
各EKSクラスタにOIDC Providerを作成
EKSクラスタ自体はterraformで管理しているので,この辺の作業は全部terraformで行う.
以下のようにクラスタを定義しているとする.
resource "aws_eks_cluster" "cluster" {
name = "${var.name}-${var.env}"
role_arn = data.terraform_remote_state.aws_iam.outputs.eks_master_role_arn
vpc_config {
security_group_ids = [
aws_security_group.eks_master.id,
]
subnet_ids = [
module.vpc.public_subnet_1_id,
module.vpc.public_subnet_2_id,
module.vpc.public_subnet_3_id,
]
}
}
まず,OpenID ConnectのProviderを作成する.
resource "aws_iam_openid_connect_provider" "oidc" {
url = aws_eks_cluster.cluster.identity.0.oidc.0.issuer
client_id_list = [
"sts.amazonaws.com"
]
thumbprint_list = [
data.external.thumb.result.thumbprint
]
}
client_id_list はいいとして,このときにthumbprintを指定する.こいつはterraformのattributeとして取得することができない.
というわけで,kubergruntという,別のCLIツールを使って取得させる.
terraformでは外部コマンドを呼び出すexternalというリソースがあるので,それを使って,
data "external" "thumb" {
program = ["kubergrunt", "eks", "oidc-thumbprint", "--issuer-url", aws_eks_cluster.cluster.identity.0.oidc.0.issuer]
}
としておく.
もちろん,kubergrunt自体をインストールする必要があるので,入れておく.
これをapplyすれば,OIDC Providerは作成できる.
OIDC ProviderごとにAssumeRoleを作成
次に,OIDCからAssumeRoleできるように,IAM Roleを作成する.
AssumeRoleに付与するPolicyはこんなの.
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "${provider_arn}" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringLike": { "${issue_hostpath}:sub": "system:serviceaccount:${namespace}:${service_account_name}" } } } ] }
だいたいこれをOIDC Providerの数だけ作る必要があるので,これをtemplateにしておく.
data "template_file" "my_cluster_assume_role_policy" {
# 先程定義したPolicyのパス
template = file(
"${path.module}/aws_iam_role_policies/irsa_assume_role_policy.json.tpl",
)
vars = {
provider_arn = "arn:aws:iam::123456789:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/A6SD4HSDFL91CDBNAKDFG67J3H"
issue_hostpath = "oidc.eks.ap-northeast-1.amazonaws.com/id/A6SD4HSDFL91CDBNAKDFG67J3H"
namespace = "my-namespace"
service_account_name = "my-iam-role-for-pod"
}
}
で,IAM Roleを作っておく.
resource "aws_iam_role" "my_pod_role" {
name = "my-pod-role"
path = "/"
assume_role_policy = data.template_file.my_cluster_assume_role_policy.rendered
}
IRSAにより認証が成功すると,Podは my_pod_role として振る舞えるので,こいつに必要な権限を付与しておく.
resource "aws_iam_policy" "s3_read_only_policy" {
name = "s3-read-only"
path = "/"
description = ""
policy = file("aws_iam_policies/s3_read_only_policy.json")
}
resource "aws_iam_policy_attachment" "s3_read_only" {
name = "s3-read-only"
roles = [
aws_iam_role.my_pod_role.name,
]
policy_arn = aws_iam_policy.s3_read_only_policy.arn
}
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowUserToS3ReadOnly", "Effect": "Allow", "Action": [ "s3:Get*", "s3:List*" ], "Resource": [ "*" ] } ] }
これで認証が成功すると,PodからはS3のオブジェクトに対して,Get, List ができるようになる.
ServiceAccountを作成
次にkubernetes側に,先のIAM Roleを関連付けたServiceAccountを作成する.
apiVersion: v1 kind: ServiceAccount metadata: name: my-iam-role-for-pod namespace: my-namespace annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/my-pod-role
あとは,このSeviceAccountをPodで利用するだけ.
apiVersion: v1 kind: Deployment metadata: name: my-pod namespace: my-namespace spec: replicas: 1 template: spec: serviceAccountName: my-iam-role-for-pod containers: # ...
というような感じ.
これで,PodからはAWSへの認証が成功するようになる.
実運用上の工夫
AssumeRolePolicyの指定項目が細かすぎる
先の例では,AssumeRolePolicyの条件に,namespaceとService Account名までを指定していた. ServiceAccountごとにAssumeRolePolicyを分離するのであれば,確かにこれでも良いのだが,実運用を考えたときにそこまでやるか?というのはある.
認証するIAM Roleは分けたいが,AssumeRolePolicyまでをService Accountごとに作成するというのはかなりめんどくさい.しかも,それで守られるというのものはそこまで大きくなく,例えば,ひとつのAssumeRolePolicyでありながらConditionsで複数の条件を書いてしまっても同じなわけだ.
であるなら,そこまで厳密にPolicyを分割する必要はない.
というわけで,例えば
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "${provider_arn}" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringLike": { "${issue_hostpath}:sub": "system:serviceaccount:*:${sa_prefix}*" } } } ] }
data "template_file" "my_cluster_assume_role_policy" {
# 先程定義したPolicyのパス
template = file(
"${path.module}/aws_iam_role_policies/irsa_assume_role_policy.json.tpl",
)
vars = {
provider_arn = "arn:aws:iam::123456789:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/A6SD4HSDFL91CDBNAKDFG67J3H"
issue_hostpath = "oidc.eks.ap-northeast-1.amazonaws.com/id/A6SD4HSDFL91CDBNAKDFG67J3H"
sa_prefix = "my-iam-role"
}
}
とかしておくと,my-iam-roleが先頭についたService AccountのみがIRSAの対象となるようになる.
この例ではnamespaceも * にしてしまっているが,この辺はどの程度緩和するかを運用に合わせて決めたらいいと思う.
困ったこと
IRSAのtokenはrootユーザでマウントされる問題
IAM Role for Service Accountは,上記のようにして設定したService Accountをもとに,IAM RoleにアクセスするためのCredentialsを取得し,それをVolumeMountしてPod内から参照できるような形を提供する.
実際には,
AWS_WEB_IDENTITY_TOKEN_FILEAWS_ROLE_ARN
という環境変数が出力され,token自体は aws-iam-tokenというVolumeがマウントされているはずだ.
その結果として何が起こるかというと,このVolume内のファイルはrootとして作成され,マウントされたあとも所有者はrootとなっている.そのため,例えばaws-sdkを実行するコンテナのユーザがrootではない場合,これらのファイルにはアクセスできないということになる.
実際適当なコンテナでやってみると,aws-sdkが認証しようとしたタイミングでaws-iam-token内のファイルにアクセスし,権限が足らずにエラーになる.
$ aws s3 ls [Errno 13] Permission denied: '/var/run/secrets/eks.amazonaws.com/serviceaccount/token'
/var/run/secrets/eks.amazonaws.com/serviceaccount の中身はこうなっているので仕方がない.
drwxrwxrwt 3 root root 100 Nov 29 14:50 . drwxr-xr-x 3 root root 28 Nov 29 14:50 .. drwxr-xr-x 2 root root 60 Nov 29 14:50 ..2019_11_29_14_50_47.379855194 lrwxrwxrwx 1 root root 31 Nov 29 14:50 ..data -> ..2019_11_29_14_50_47.379855194 lrwxrwxrwx 1 root root 12 Nov 29 14:50 token -> ..data/token
これを回避するためには,fsGroupを使う.
apiVersion: v1 kind: Deployment metadata: name: my-pod namespace: my-namespace spec: replicas: 1 template: spec: serviceAccountName: my-iam-role-for-pod securityContext: fsGroup: 1000 containers: # ...
こしてやると,
drwxrwsrwt 3 root operation 100 Nov 29 14:53 . drwxr-xr-x 3 root root 28 Nov 29 14:53 .. drwxr-sr-x 2 root operation 60 Nov 29 14:53 ..2019_11_29_14_53_43.467433262 lrwxrwxrwx 1 root root 31 Nov 29 14:53 ..data -> ..2019_11_29_14_53_43.467433262 lrwxrwxrwx 1 root root 12 Nov 29 14:53 token -> ..data/token
こうなり,無事にawsコマンドが使えるようになる.
ちなみに,Dockerを本番で利用するに当たって,root以外のユーザでコンテナを実行するのは割とよくやる手法で,セキュリティ的な懸念が大きい.コンテナ内のrootユーザは,ホストOS上でもrootユーザとして振る舞える.もしコンテナ内に侵入された場合,コンテナ内をrootで書き換えられることは当たり前として,VolumeMountしていたホスト側もrootで操作できるということになる. これを避けるために本番ではroot以外のユーザを使っていたりする.
要求されるaws-sdkのバージョン
ここまでくれば残りは些細な問題で,IAM Role for Service Accountを使うために必要なところまでawscliやaws-sdkのバージョンを上げるだけだ.
ただ,ここが結構大変で,当然運用中のKubernetesクラスタ上には無数のPodが動いている.複数のチームがある場合は当然それぞれアプリケーションを作っているわけで,その中で使っているaws-sdkのバージョンをすべてチェックして,バージョンアップするのはかなり骨が折れる.
また,バージョンアップを忘れたままIRSAに移行してしまうと,単純にaws-sdkが認証情報を取れなくなるので,No Credentials Errorが発生したりする.移行のときにこのようなエラーが出た場合は,まず,aws-sdkのバージョンを疑おう.
必要になるバージョンは公式ドキュメントに記述されている.
また,自分のチームで運用しているアプリであれば,バージョンアップの労力をかければ上げることは可能だが,これがOSSなどを使っていると大変だ.OSS側でaws-sdkのバージョンが上がるのを待つ必要があり,それまではIRSAに変えることができない.
まとめ
VolumeMountがrootでされてしまう問題以外は,そこまでハマることなく移行することができた.ただ,一部OSS利用している箇所についてはまだaws-sdkのバージョンアップができておらず,kube2iamを使っている部分が残っている.
ただIRSAに変更した部分はkube2iamのようなバグを踏むことなくAWSの認証に成功している.