kOpsでIAM Role for Service Account(IRSA)をやる(1.23)

以前こういう記事を書いた.

h3poteto.hatenablog.com

このときから更に便利になり,kOps 1.23で,ついにeks-pod-identity-webhookさえも自動インストールできるようになったので紹介する.

github.com

PodIdentityWebhookをインストールする

cluster_specに

spec:
  certManager:
    enabled: true
  podIdentityWebhook:
    enabled: true

と書くだけで良い.ちなみに,webhookなので証明書が必要になるのだが,これを管理するためにcertManagerは必須となっている.なので certManager.enabled: trueはお忘れなく.

この状態でクラスタを作ると,

$ kubectl get pods -n kube-system
NAME                                                                        READY   STATUS    RESTARTS        AGE
cert-manager-84955789dd-g2djl                                               1/1     Running   0               3m54s
cert-manager-cainjector-6475d566f7-jtx99                                    1/1     Running   0               3m54s
cert-manager-webhook-7b7b64cc86-t64dk                                       1/1     Running   0               3m54s

# 中略

pod-identity-webhook-565f794b7c-gffkd                                       1/1     Running   0               3m54s
pod-identity-webhook-565f794b7c-scrf8                                       1/1     Running   0               3m54s

このようにpod-identity-webhookが自動的にデプロイされる.

IRSAの使い方まとめ

セットアップ

最終的にどういう手順になったのかを書いておく.

  1. publicなS3 bucketを作る
  2. spec.serviceAccountIssuerDiscovery をcluster_specに指定
  3. (Optional) spec.iam.serviceAccountExternalPermissionをcluster_specに指定
  4. spec.podIdentityWebhook をcluster_specに指定
  5. kopsコマンドでクラスタを作るだけ
spec:
  serviceAccountIssuerDiscovery:
    discoveryStore: s3://public-readable-s3-bucket-name
    enableAWSOIDCProvider: true
  iam:
    serviceAccountExternalPermissions:
      - name: my-sa
        namespace: my-ns
        aws:
          inlinePolicy: |-
            [
              {
                "Effect": "Allow",
                "Action": "ec2:Describe*",
                "Resource": "*"
              }
            ]
  podIdentityWebhook:
    enabled: true
  certManager:
    enabled: true

これだけ.

使い方

ServiceAccountを作成

クラスタができたらServiceAccountを作っておく.

apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::00000000:role/my-sa.my-ns.sa.cluster-name
  name: my-sa
  namespace: my-ns

spec.iam.serviceAccountExternalPermissionsを指定している場合 eks.amazonaws.com/role-arn は自動的に作成されるわけだが,これは以下のようなコマンドで探してくる.

$ aws iam get-role --role-name my-sa.my-ns.sa.cluster-name
{
    "Role": {
        "Path": "/",
        "RoleName": "my-sa.my-ns.sa.cluster-name",
        "RoleId": "***********",
        "Arn": "arn:aws:iam::00000000:role/my-sa.my-ns.sa.cluster-name",
        "CreateDate": "2022-03-17T12:25:46+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Federated": "arn:aws:iam::00000000:oidc-provider/public-readable-s3-bucket-name.s3.us-east-1.amazonaws.com"
                    },
                    "Action": "sts:AssumeRoleWithWebIdentity",
                    "Condition": {
                        "StringEquals": {
                            "public-readable-s3-bucket-name.s3.us-east-1.amazonaws.com:sub": "system:serviceaccount:my-ns:my-sa"
                        }
                    }
                }
            ]
        },
    }
}

注意: spec.iam.serviceAccountExternalPermissionを指定しない場合,これに相当するIAM Roleは自分で作る必要がある. terraformで作る場合は以下のようなものになる.

resource "aws_iam_role" "my-sa" {
  name = "my-sa.my-ns.sa"
  path = "/"

  inline_policy {
    name = "my_inline_policy"

    policy = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action   = ["ec2:Describe*"]
          Effect   = "Allow"
          Resource = "*"
        },
      ]
    })
  }  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::00000000:oidc-provider/public-readable-s3-bucket-name.s3.us-east-1.amazonaws.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "public-readable-s3-bucket-name.s3.us-east-1.amazonaws.com:sub": "system:serviceaccount:my-ns:my-sa"
        }
      }
    }
  ]
}
EOF
}

このとき,Federatedに指定する値は

$ aws iam list-open-id-connect-providers
{
    "OpenIDConnectProviderList": [
        {
            "Arn": "arn:aws:iam::00000000:oidc-provider/public-readable-s3-bucket-name.s3.us-east-1.amazonaws.com"
        }
    ]
}

のようにして探してくる.

動作確認

こんなJobを作ってみる.

apiVersion: batch/v1
kind: Job
metadata:
  name: awscli
  namespace: my-ns
spec:
  template:
    spec:
      restartPolicy: Never
      serviceAccountName: my-sa
      containers:
        - name: awscli
          image: amazon/aws-cli:latest
          args: ["ec2", "describe-instances", "--region", "ap-northeast-1"]
  backoffLimit: 0

これをapplyすると

spec:
  containers:
  - args:
    - ec2
    - describe-instances
    - --region
    - ap-northeast-1
    env:
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::564677439943:role/my-sa.my-ns.sa.playground.k8s.h3poteto.dev
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
    image: amazon/aws-cli:latest
    imagePullPolicy: Always
    name: awscli
    # Omission
  volumes:
  - name: aws-iam-token
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          audience: amazonaws.com
          expirationSeconds: 86400
          path: token

このように AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILEがenvに設定される. また,projected volumeがマウントされている.

というわけでログをみると

$ kubectl logs -f awscli-xbksl -n my-ns
{
    "Reservations": [
        {
            "Groups": [],
            "Instances": [
                {
                    "AmiLaunchIndex": 0,
                    "ImageId": "ami-********",
                    "InstanceId": "i-0f4e3e20cf27d652c",
                    "InstanceType": "t3.medium",
                    "KeyName": "******",
                    "LaunchTime": "2022-03-11T15:25:05+00:00",
                    "Monitoring": {
                        "State": "disabled"
                    },
                    "Placement": {
                        "AvailabilityZone": "ap-northeast-1c",
                        "GroupName": "",
                        "Tenancy": "default"
                    },

無事awsコマンドが成功している.

まとめ

ようやくkOpsでもEKSと大差ないくらいの手順でIRSAが利用できるようになった. あと,eks-pod-identity-webhook側がcert-manager対応してくれたおかげで,ここまで来ることができた.長らくeks-pod-identity-webhookは独自で証明書管理をしていたのだが,これが分離されたことにより単体でインストールする場合もかなり楽になったんではないだろうか.

github.com