Terraformを使ってAWS SSOのユーザにEKSクラスターのフルアクセス権限を与える

2021.03.22

やりたいこと

AWS SSOのユーザーに、特定のEKSクラスターのフルアクセス権限を付与する設定を、Terraformで行ないたいと思います。

詳細

厳密に言うと、SSOのユーザーは、アクセス権限セットで作成されるIAM Roleにスイッチロールして当該アカウントにログインしますので、フルアクセス権限を与えるエンティティもIAM Roleになります。このあたりの仕組みをよくご存じない方は以下のエントリを御覧ください。

また、「フルアクセス権限を付与する」というのも、厳密には当該IAM Roleをk8sクラスター内のsystem:mastersグループに追加するだけです。system:mastersグループがフルアクセス権限を持っているcluster-adminClusterRoleと紐付いています。

`$ kubectl get clusterrole cluster-admin -o yaml`の結果

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  creationTimestamp: "2021-03-12T06:33:31Z"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  managedFields:
  - apiVersion: rbac.authorization.k8s.io/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .: {}
          f:rbac.authorization.kubernetes.io/autoupdate: {}
        f:labels:
          .: {}
          f:kubernetes.io/bootstrapping: {}
      f:rules: {}
    manager: kube-apiserver
    operation: Update
    time: "2021-03-12T06:33:31Z"
  name: cluster-admin
  resourceVersion: "46"
  selfLink: /apis/rbac.authorization.k8s.io/v1/clusterroles/cluster-admin
  uid: eaf54afc-0b97-4607-bb60-79c2cec01f8f
rules:
- apiGroups:
  - '*'
  resources:
  - '*'
  verbs:
  - '*'
- nonResourceURLs:
  - '*'
  verbs:
  - '*'

rules欄がすべて*(全許可)になっています。

ゴール

具体的な対応内容は、kube-systemnamespaceに存在する aws-authというConfigMapに前述のSSOで作成されるIAM Roleに関する設定を書くというものになります。

以下公式ドキュメントに書かれているように、mapRolesセクション内にrolearnusernamegroupsという項目値を追記します。

やってみた

前提条件

AWS SSO側での作業が終わっていることとします。つまり以下データが作成済であるということです。

  • ユーザー
  • アクセス権限セット
  • 割り当て(ユーザー/アカウント/アクセス権限セットの紐付け)

今回は、AdministratorAccess AWS管理ポリシーをアタッチしたAdministratorAccessアクセス権限セットを作っています。

EKSクラスターの作成

以下の方法で作成します。

IAM RoleのARNを取得

さて、次はSSOで作成されるIAM RoleのARNをTerraform内で参照出来るようにしましょう。最終的にそのARNをaws-authConfigMap内のmapRolesセクション内のrolearnに書きます。

Data Sourceではできなかった

TerraformにはData Sourceという、既存リソースの情報を参照するための方法があり、IAM Roleについても用意されているのでこいつを使えば楽勝だろうと思っていたのですが、無理でした。以下のように、参照したいIAM Roleの名前を完全一致で指定する必要があるためです。

data "aws_iam_role" "example" {
  name = "an_example_role_name"
}

SSOによって作成されるIAM Roleの名前は、AWSReservedSSO_(アクセス権限セット名)_(英字小文字と数字の組み合わせのランダムな文字列)となります。最後にランダムな文字列が付くので、名前完全一致を実現するのは難しいです。

ダメもとで以下のような部分一致のコードを実行(terraform apply)してみましたが、

data "aws_iam_role" "sso_admin_role" {
  name = "AWSReservedSSO_AdministratorAccess_*"
}

エラーになりました。

Error: error reading IAM Role (AWSReservedSSO_AdministratorAccess_*): ValidationError: The specified value for roleName is invalid. It must contain only alphanumeric characters and/or the following: +=,.@_-

仕方が無いので別のアプローチで攻めます。色々試行錯誤した結果、以下の構成になりました。

  1. AWS CLIでIAM Roleの情報をざっくり取得
  2. jqで絞り込み
  3. external providerを使ってterraform内で利用する

AWS CLI

IAM RoleのARNを取得するには、aws iam list-rolesを使って一覧をざっくり取得する必要があります。aws iam get-roleもありますが、これは必須引数でロール名があるので今回の要件には合いませんでした。

$ aws iam list-roles --query 'Roles[*].{RoleName:RoleName,Arn:Arn}' --path-prefix /aws-reserved/sso.amazonaws.com/
[
    {
        "RoleName": "AWSReservedSSO_AdministratorAccess_51de502f7e28cecb",
        "Arn": "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/ap-northeast-1/AWSReservedSSO_AdministratorAccess_51de502f7e28cecb"
    },
    {
        "RoleName": "AWSReservedSSO_ReadOnlyAccess_13403f71ae721770",
        "Arn": "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/ap-northeast-1/AWSReservedSSO_ReadOnlyAccess_13403f71ae721770"
    },
    {
        "RoleName": "AWSReservedSSO_ViewOnlyAccess_c2ac1decdda72cbc",
        "Arn": "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/ap-northeast-1/AWSReservedSSO_ViewOnlyAccess_c2ac1decdda72cbc"
    }
]
  • --queryで必要な項目だけ取得するようにしています。
  • --path-prefixでAWS SSO経由で作成されたIAM Roleだけを取得するように絞り込んでいます。

これだけだとSSOで作成された他のIAM Roleもヒットしてしまっています。jqを使って絞り込んでいきます。

jq

$ aws iam list-roles --query 'Roles[*].{RoleName:RoleName,Arn:Arn}' --path-prefix /aws-reserved/sso.amazonaws.com/ \
| jq '.[] | select(.RoleName |test("AWSReservedSSO_AdministratorAccess_*"))'
{
  "RoleName": "AWSReservedSSO_AdministratorAccess_51de502f7e28cecb",
  "Arn": "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/ap-northeast-1/AWSReservedSSO_AdministratorAccess_51de502f7e28cecb"
}

先程のAWS CLIの結果をjqに渡しています。test()で正規表現が使えます。select((検索対象文字列)| test((正規表現)))とすることで、検索対象文字列が正規表現にマッチする配列要素だけを取得できます。

Arnの取得方法については目処が付きました。

external provider

external providerはコマンド結果を参照できるようになるTerraformのproviderです。これを使って先程のコマンドの結果をTerraformの世界に取り込みます。

まず先程のコマンドをbashスクリプト化します。

./modules/k8s_cluster/bash/get-sso-admin-role.sh

aws iam list-roles --query 'Roles[*].{RoleName:RoleName,Arn:Arn}' --path-prefix /aws-reserved/sso.amazonaws.com/ | jq '.[] | select(.RoleName |test("AWSReservedSSO_AdministratorAccess_*"))'

そしてexternal data sourceで参照します。

./modules/k8s_cluster/eks.tf追記

data "external" "get_sso_admin_role" {
  program = ["bash", "./bash/get-sso-admin-role.sh"]
}

required_providersの設定も忘れずに。

/modules/k8s_cluster/main.tf

terraform {
  required_providers {
    external = {
      source = "hashicorp/external"
      version = "2.1.0"
    }
  }
}

この状態で terraform initし直してterraform applyをすることで、コマンド結果をTerraform内で扱うことが出来るようになります。

$ terraform state show module.blue.data.external.get_sso_admin_role
# module.blue.data.external.get_sso_admin_role:
data "external" "get_sso_admin_role" {
    id      = "-"
    program = [
        "bash",
        "./bash/get-sso-admin-role.sh",
    ]
    result  = {
        "Arn"      = "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/ap-northeast-1/AWSReservedSSO_AdministratorAccess_51de502f7e28cecb"
        "RoleName" = "AWSReservedSSO_AdministratorAccess_51de502f7e28cecb"
    }
}

aws-auth ConfigMapの更新

EKS published moduleを使っている場合、今回更新が必要なaws-auth ConfigMapのmapRolesセクションの設定はmoduleの引数から可能です。map_roles引数を使います。

 module "main" {
  source  = "terraform-aws-modules/eks/aws"
  version = "14.0.0"

  cluster_name    = var.cluster_name
  cluster_version = var.k8s_version
  subnets         = var.subnet_ids
  vpc_id          = var.vpc_id

  node_groups = {
    example = {
      target_group_arns = [aws_alb_target_group.tg.arn]
      subnets           = var.private_subnet_ids
    }
  }

+  map_roles = local.map_roles
 }

+locals {
+  map_roles = [
+    {
+      rolearn  = data.external.get_sso_admin_role.result.Arn
+      username = "${data.external.get_sso_admin_role.result.RoleName}:{{SessionName}}"
+      groups   = ["system:masters"]
+  }]
+}

 data "external" "get_sso_admin_role" {
   program = ["bash", "./modules/k8s_cluster/bash/get-sso-admin-role.sh"]
 }

{{SessionName}}は動的にassume roleのセッション名に変換されます。これにより、スイッチロール元のユーザーを識別することができます。詳細は以下GREEさんのブログに記載されています。

接続確認

以上で設定完了なので、SSOユーザーで接続確認してみましょう。

AWSでの認証

まずAWS側の認証が必要です。CLIをSSOユーザー(がスイッチするIAM Role)の権限で利用できるように設定します。aws configure ssoコマンドを使います。

$ aws configure sso
SSO start URL [None]: https://d-xxxxxxxxxx.awsapps.com/start
SSO Region [None]: ap-northeast-1
Attempting to automatically open the SSO authorization page in your default browser.
If the browser does not open or you wish to use a different device to authorize this request, open the following URL:

https://device.sso.ap-northeast-1.amazonaws.com/

Then enter the code:

NLXV-FDJG

自動でブラウザでSSOポータルのログイン画面が開くのでログインします。 20210321-sso-login-form この画面まで進めたらCLIに戻ります。 20210321-sso-login-succeed

どのアカウントかや、どのIAMロールを使うのかなど色々訊かれるので答えていきます。

There are 3 AWS accounts available to you.
Using the account ID 123456789012
There are 2 roles available to you.
Using the role name "AdministratorAccess"
CLI default client Region [ap-northeast-1]:
CLI default output format :
CLI profile name [AdministratorAccess-123456789012]: eks-access-test

To use this profile, specify the profile name using --profile, as shown:

aws s3 ls --profile eks-access-test

これでAWSの認証ができました。数時間(デフォルト一時間)で認証が切れますが、その場合は $ aws sso login --profile eks-access-testコマンドを実行してください。

kubeconfigファイルの作成

$ aws eks update-kubeconfig --name blue-with-module --profile eks-access-test

これで~/.kube/configに設定が追記されます。ちゃんとeks-access-testプロファイルを使うように書かれていますね。

- name: arn:aws:eks:ap-northeast-1:123456789012:cluster/blue-with-module
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      args:
      - --region
      - ap-northeast-1
      - eks
      - get-token
      - --cluster-name
      - blue-with-module
      command: aws
      env:
      - name: AWS_PROFILE
        value: eks-access-test

エラー

いよいよ接続です。が、Unauthorizedエラーになります。

$ kubectl config current-context
arn:aws:eks:ap-northeast-1:123456789012:cluster/blue-with-module
$ kubectl get all -A
error: You must be logged in to the server (Unauthorized)

他のユーザーで接続してConfigMapを確認してみましたが、正しく設定されているように見えます。

$ kubectl get configmap aws-auth -o yaml -n kube-system
apiVersion: v1
data:
  mapAccounts: |
    []
  mapRoles: |
    - "groups":
      - "system:bootstrappers"
      - "system:nodes"
      "rolearn": "arn:aws:iam::123456789012:role/blue-with-module20210321061439521900000009"
      "username": "system:node:{{EC2PrivateDNSName}}"
    - "groups":
      - "system:masters"
      "rolearn": "arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/ap-northeast-1/AWSReservedSSO_AdministratorAccess_51de502f7e28cecb"
      "username": "AWSReservedSSO_AdministratorAccess_51de502f7e28cecb:{{SessionName}}"
  mapUsers: |
    []
kind: ConfigMap
metadata:
  creationTimestamp: "2021-03-21T06:17:32Z"
  labels:
    app.kubernetes.io/managed-by: Terraform
    terraform.io/module: terraform-aws-modules.eks.aws
  managedFields:
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      f:data:
        .: {}
        f:mapAccounts: {}
        f:mapRoles: {}
        f:mapUsers: {}
      f:metadata:
        f:labels:
          .: {}
          f:app.kubernetes.io/managed-by: {}
          f:terraform.io/module: {}
    manager: HashiCorp
    operation: Update
    time: "2021-03-21T06:17:32Z"
  name: aws-auth
  namespace: kube-system
  resourceVersion: "34150"
  selfLink: /api/v1/namespaces/kube-system/configmaps/aws-auth
  uid: 66cc3516-49aa-4abe-9b26-064377325da1

path-prefixの除去が必要

以下のQiita記事で言及されていたのですが、rolearnとして記述するIAM RoleのARNから/aws-reserved/sso.amazonaws.com/というpath-prefixを削除する必要があるようです。

今回の場合、東京リージョンでSSOを有効化したので、path-prefixは/aws-reserved/sso.amazonaws.com/ap-northeast-1/となっています。

というわけで、tfファイルを編集します。

 locals {
   map_roles = [
     {
-      rolearn  = data.external.get_sso_admin_role.result.Arn
+      rolearn  = replace(data.external.get_sso_admin_role.result.Arn, "aws-reserved/sso.amazonaws.com/ap-northeast-1/", "")
       username = "${data.external.get_sso_admin_role.result.RoleName}:{{SessionName}}"
       groups   = ["system:masters"]
   }]
 }

terraform applyをして再度確認すると、無事接続成功しました。👏

$ kubectl get all -A
NAMESPACE     NAME                           READY   STATUS    RESTARTS   AGE
kube-system   pod/aws-node-sqpvb             1/1     Running   0          5h14m
kube-system   pod/coredns-59847d77c8-mp8t2   1/1     Running   0          5h20m
kube-system   pod/coredns-59847d77c8-twzdr   1/1     Running   0          5h20m
kube-system   pod/kube-proxy-cb65z           1/1     Running   0          5h14m

NAMESPACE     NAME                 TYPE           CLUSTER-IP       EXTERNAL-IP                                                                    PORT(S)         AGE
default       service/kubernetes   ClusterIP      172.20.0.1       <none>                                                                         443/TCP         5h20m
kube-system   service/kube-dns     ClusterIP      172.20.0.10      <none>                                                                         53/UDP,53/TCP   5h20m

NAMESPACE     NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
kube-system   daemonset.apps/aws-node     1         1         1       1            1           <none>          5h20m
kube-system   daemonset.apps/kube-proxy   1         1         1       1            1           <none>          5h20m

NAMESPACE     NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
kube-system   deployment.apps/coredns   2/2     2            2           5h20m

NAMESPACE     NAME                                 DESIRED   CURRENT   READY   AGE
kube-system   replicaset.apps/coredns-59847d77c8   2         2         2       5h20m

ログ確認

aws-authConfigMap内に書いた{{SessionName}}が正しく変換されているかどうかも確認しておきます。まずは監査ログを吐くようにクラスターの設定を変更します。

 module "main" {
   source  = "terraform-aws-modules/eks/aws"
   version = "14.0.0"
 
   cluster_name    = var.cluster_name
   cluster_version = var.k8s_version
   subnets         = var.subnet_ids
   vpc_id          = var.vpc_id
 
   node_groups = {
     example = {
       target_group_arns = [aws_alb_target_group.tg.arn]
       subnets           = var.private_subnet_ids
     }
   }
 
   map_roles = local.map_roles

+  cluster_enabled_log_types = ["audit"]
 }

しばらくするとCloudWatch Logsにログが生成されだします。

CloudWatch Logsコンソールにて、変換されていることを確認できました。 20210321-audit

環境情報

最後に今回の実行環境の情報です。

  • Terraform 0.14.7
    • AWS provider 3.30.0
    • kubernetes provider 2.0.2
    • local provider 2.1.0
    • null provider 3.1.0
    • random provider 3.1.0
    • template provider 2.2.0
    • external provider 2.1.0
  • awscli aws-cli/2.1.26 Python/3.7.4 Darwin/19.6.0 exe/x86_64 prompt/off
  • jq jq-1.6