kopsで構築したKubernetesクラスタでIRSAを使う

IAM Role for Service Account(IRSA)自体はEKS以外でも利用可能ということだったので,自前のkopsクラスタで構築してみた. ほぼ,こちらのお世話になっている.

blog.hatappi.me

ほぼ同じことをしているのだが,いかんせん複雑なので自分用の記録として残しておく.

OIDC Providerを作成

IssuerをS3上に作る

こちらのガイドでは,OIDCのissuerを公開するために,S3のバケットを作っている. もちろんこれはissuerとして振る舞って,必要なファイルを配信しれくれればS3でなくても問題ない.

大事なことは,次で説明する鍵を作成し,これを元にした discovery.json と,keys.json を,

  • https://${ISSUER_HOSTPATH}/.well-known/openid-confugration
  • https://${ISSUER_HOSTPATH}/keys.json

というURLで配信できていることである.

S3で行う場合,このBucketus-east-1リージョンに作ると良い. S3のBucketus-east-1 だけ特別扱いされており, bucket-name.s3.amazonaws.com というようなドメインでアクセスすることができるのはus-east-1だけである.

これをTokyoに作ったりすると, s3-ap-northeast-1.amazonaws.com/bucket-name という形になってしまう.まぁどうしてもTokyoに作る場合は,この形式でも問題ないのかもしれないが……あとは,Static Website HostingするとかCloudFrontを通すとか,やり方はいろいろある.いずれにしろ,自分が指定したドメインで,↑のファイルが配信できていれば良い.

resource "random_uuid" "oidc_s3" {
}

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

  versioning {
    enabled = true
  }
}

output "oidc_website_endpoint" {
  value = "${aws_s3_bucket.oidc.bucket_domain_name}"
}

こんな定義を書いてus-east-1にapplyした.なお,ファイルを公開する必要があるので,当然BucketはObjects can be publicである必要がある.

鍵を作る

$ 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

この鍵は,discovery.jsonkeys.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
$ 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

この2つのファイルは,先程作成したS3 Bucketにアップロードしておく.なお, discovery.json/.well-known/openid-configuration としてアップロードする必要があるので注意.

鍵をKubernetesのmasterインスタンスに配置

これは参考記事の通り.

kopsの定義として,

  fileAssets:
  - content: |
      LS0tL...
    isBase64: true
    name: service-account-signing-key-file
    path: /srv/kubernetes/assets/service-account-signing-key
  - content: |
      LS0tL...
    isBase64: true
    name: service-account-key-file
    path: /srv/kubernetes/assets/service-account-key
  kubeAPIServer:
    apiAudiences:
    - oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com
    serviceAccountIssuer: https://oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com
    serviceAccountKeyFile:
    - /srv/kubernetes/server.key
    - /srv/kubernetes/assets/service-account-key
    serviceAccountSigningKeyFile: /srv/kubernetes/assets/service-account-signing-key

こんな感じになる.2020年4月10日 kops 1.16.0現在,

github.com

このIssueはすでに解決済みになっており,すでにserviceAccountKeyFileは複数指定できるので,このままで問題ない.

また,これらをmasterのノードに配置する必要があるということは,当然updateした後にrolling-updateまでして,masterノードを更新してやる必要がある.

$ kops update cluster --name your_cluster_name --yes
$ kops rolling-update cluster your_cluster_name --instance-group-role=Master --yes

OIDC Providerの作成

ようやくProviderを作成する段階.この作業はkopsに限らずEKSでも必要なので,まったく同じことをやる.EKSの場合は上記の鍵やIssuerはEKSクラスタ起動時に用意されている.

resource "aws_iam_openid_connect_provider" "iam_role_sa" {
  # issuerのURL
  # ここでは先程作成したus-east-1のS3 Bucketを参照している
  url = "https://${data.terraform_remote_state.us.outputs.oidc_website_endpoint}"

  client_id_list = [
    data.terraform_remote_state.us.outputs.oidc_website_endpoint
  ]

  thumbprint_list = [
    data.external.thumb.result.thumbprint
  ]
}

data "external" "thumb" {
  program = ["kubergrunt", "eks", "oidc-thumbprint", "--issuer-url", "https://${data.terraform_remote_state.us.outputs.oidc_website_endpoint}"]
}

なお,thumbprintの生成には,kubergruntを使っている.

github.com

これでOIDC Providerの作成まではきた.

Webhookを作成

github.com

このWebhookをクラスタにインストールすることで,立ち上がったPodにIRSA用のJTWや環境変数を差し込んでくれる. EKSの場合は,このWebhook自体もEKSクラスタ起動時に勝手にインストールされているものになる.

なお,公式でこのWebhook用のDocker Imageは提供されていない.そのため自前ビルドする必要がある.

https://github.com/aws/amazon-eks-pod-identity-webhook/blob/master/Makefile#L27

docker buildしてpushしているだけなので,ECRでもhub.docker.comでも,好きなところにDocker Imageをpushしたらいいと思う.kopsのクラスタからDocker pullできればどこでも良い.

make push

あとはインストールするだけなのだが,これはMakefileにラップされたkustomizeで行う. この前に,

https://github.com/aws/amazon-eks-pod-identity-webhook/blob/master/deploy/deployment-base.yaml#L28

ここを sts.amazonaws.com ではなく,oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com のようなIssuerのホストにしておく必要がある.

そして

$ make cluster-up IMAGE=1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/eks/pod-identity-webhook

https://github.com/aws/amazon-eks-pod-identity-webhook/blob/master/Makefile#L63

としてやれば,クラスタにWebhookがインストールされ,Podが起動する.

ここまでくればEKSでセットアップしたときと同様の状態になるので,あとはEKSのときとまったく同じ手順になる.

IAM Roleを作成

resource "aws_iam_role" "call_s3_role" {
  name               = "call-s3-role"
  path               = "/"
  assume_role_policy = data.template_file.irsa_assume_role_policy.rendered
}
data "template_file" "irsa_assume_role_policy" {
  template = file(
    "${path.module}/aws_iam_role_policies/irsa_assume_role_policy.json.tpl",
  )

  vars = {
    provider_arn   = "arn:aws:iam::1234567890:oidc-provider/oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com"
    issue_hostpath = "oidc-xxx-yyy-zzz-111-222.s3.amazonaws.com"
    prefix         = "my-role"
  }
}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "${provider_arn}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringLike": {
          "${issue_hostpath}:sub": "system:serviceaccount:*:${prefix}*"
        }
      }
    }
  ]
}

こんなIAM Roleを作って,このRoleに必要な権限をattachしておく.

resource "aws_iam_policy_attachment" "s3_all" {
  name = "s3-all"

  roles = [
    aws_iam_role.call_s3_role.name,
  ]

  policy_arn = aws_iam_policy.s3_get.arn
}

ServiceAccountの作成

作ったRoleをServiceAccountのannotationsに追加する.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-role-s3
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::1234567890:role/call-s3-role

で,これをPodのServiceAccountNameに指定する.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
    spec:
      serviceAccountName: my-role-s3

これでPodを起動すれば,

    Environment:
      AWS_ROLE_ARN:                 arn:aws:iam::1234567890:role/s3-call-role
      AWS_WEB_IDENTITY_TOKEN_FILE:  /var/run/secrets/eks.amazonaws.com/serviceaccount/token
    Mounts:
      /var/run/secrets/eks.amazonaws.com/serviceaccount from aws-iam-token (ro)
      /var/run/secrets/kubernetes.io/serviceaccount from sample-manager-token-9t72l (ro)

というようにIRSAに必要なVolumeMountと環境変数が差し込まれる.

こうなれば,あとは新しいaws-sdkを使っていれば,これらの情報をつかって認証してくれる.

運用してわかったこと

EKS公式のWebhookと違って,自分でWebhookをインストールしてPodを動かしているので,例えばScaleIn等でWebhookのPodがevictされていたり,ノードごと死んでいたりした場合には,Webhookが働かない. そのタイミングで運悪くPodが起動したりすると(実はScaleInやノードの死では複数Podが同時にevictされscheduleされ直すので,こういうのはよくある),そのPodにはWebhookにより必要な情報が差し込まれなかったりする.

EKSではこういうことはないのだが,自前運用であるから仕方ない.防護策としては,WebhookのDeploymentのreplicasを上げて,Podを複数個,それも複数ノードに分散させて起動したりしておくと,多少安全性は増す.流石にScaleInが激しくて複数ノードが全部一度に死んだりすると,Webhookを受け取れないタイミングが存在するかもしれないが.

まとめ

自分でもWebhookをいくつか作ったことがあるのでわかるのだが,Webhookはインストールが結構めんどくさい.これは,Webhookを送るときに必ずTLSで送る必要があり,このときに使う証明書を

  • MutatingWebhookConfigurationのclientConfig
  • Webhookを受けるPod

の両方で同じものを利用する必要があり,それをhelm chart等に落とすのが難しいという事情がある.証明書を固定にして公開しても良ければ,それこそSecretとして証明書を保存してしまえばいいのだが,あまりよくないので,Makefileを使って生成し,それを元にkustomizeでインストールしたりしている.

kustomizeを使っても,証明書生成の部分はどうしてもユーザが何かしらの方法で行う必要があるため,Makefileが必要になったりしている.

という事情は非常によくわかる.

ただ,めんどくさい.本当はhelm chart一発でインストールできたらどれほど楽かと思う. kube2iamとかkiamは,IAM Roleを用意するのはもちろん必要になるが,クラスタへのインストール自体はhelmコマンド一つでできるからね…….

インストールさえしてしまえば,IRSAの方が安定していて良いと思う.kube2iamのように,AWSへのリクエストを毎回中継して認証してやる必要はない. Webhookが,Pod起動時に必要な情報を差し込んでさえしまえば,あとはaws-sdkとOIDC ProviderとAWS側とのやりとりだけの話になるからだ.