TECHSTEP

ITインフラ関連の記事を公開してます。

External Secretsを試してみる

今回はKubernetes上で扱う秘匿情報を管理するExternal Secretsを試してみました。

注意事項:本記事はExternal SecretsのリポジトリGoDaddyからexternal-secretsに移管する前に検証をしていたものです。そのため、一部マニフェストファイルにはgodaddy/kubernetes-external-secretsと記載されております。

リポジトリ移管の経緯はGitHub Issueに記載されております。

Kubernetesで利用するSecretの課題と関連プロダクト

Kubernetesはログインパスワードなどの秘匿情報をSecretリソースで管理することが一般的な方法です。Secretリソースはbase64で暗号化した値を設定する必要があり、これはSecretリソースにアクセスできてしまえば、簡単に復号化できてしまう情報になります。

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  username: YWRtaW4=  # admin
  password: YWRtaW5wYXNzd29yZAo=  # adminpassword
$ echo YWRtaW4= | base64 --decode
admin
$ echo YWRtaW5wYXNzd29yZAo= | base64 --decode
adminpassword

一方でKubernetesマニフェストファイルをGitHubで管理してGitOpsを行う場合、Secretを含むマニフェストファイルをGitHubリポジトリに保管するため、リポジトリにアクセスできるユーザーは誰でも秘匿情報を知ることができてしまいます。

このようなケースに対応するため、KubernetesのSecret情報を管理するプロダクトは複数開発されております。代表的なものとして、以下のものが挙げられます。

  • kubesec: gpg / AWS KMS / Google Cloud KMSなどの鍵情報を利用してSecretリソースを暗号化する。kubesecコマンドを実行して新規・既存のSecretリソースを作成することができる。
  • Sealed Secret: SealedSecret Operatorとkubesealコマンド、デプロイ時にクラスター上に作成される公開鍵・秘密鍵を組み合わせる。kubesealコマンドによってSecretリソースを暗号化したテンプレートが用意され、そのテンプレートを利用してSealedSecretリソースを作成することでSecretリソースが作成される。
  • External Secrets: ExternalSecrets Controllerを利用して、外部のサービスに保管された秘匿情報からSecretリソースを作成する。
  • Berglas: Google Cloud Storage / Google Cloud KMS等を利用して秘匿情報を管理するツール。Kubernetes Secretリソースに対してはMutatingWebhookConfigurationを利用して、Google Cloud Secret Managerに保管した秘匿情報をDeploymentなどのリソースに渡す。

上記ツールの比較については、以下の記事を参照してください。

※参考ドキュメント:

External Secretsとは

このうちExternal Secretsは、以下のような特徴を備えたツールです。

外部の秘匿情報管理サービスから情報を取得

External Secretsは、クラウド上のサービスや、秘匿情報管理ツールなどから情報を取得し、それを用いてKubernetes上でSecretリソースを作成します。秘匿情報の保管はKubernetes以外の外部サービスを利用することで、Kubernetes向けのマニフェストファイル内に直接データを設定する必要がなくなります。

またExternal Secretsはデフォルトで10sごとに外部サービスを確認し、外部サービスの設定が変更された場合、その変更をSecretに反映します。そのため、Kubernetes側での設定変更は不要となります。

※任意のタイミングでSecretを更新したい場合は、以下のリンク先を参照ください。

ExternalSecret リソースの利用

External SecretsではExternalSecretという独自リソースをCRDで定義し利用します。ExternalSecretを作成すると、External Secretsの起動したPodから、指定した外部サービスにアクセスし、データを取得します。取得後はそのデータを用いてSecretリソースを作成することで、Kubernetes上のリソースから秘匿情報を利用することが可能になります。

複数のクラウドプロバイダーに対応

External Secretsは現在以下のサービス・製品に対応しています。複数のクラウドプロバイダーに対応しているため、様々な環境に対して共通利用できることが期待できます。

※External Secrets概要図(GitHubより)

external-secret-summary

External Secretsを使ってみる

ここからは実際にExternal Secretsを利用する方法を紹介します。利用自体はとてもシンプルなので、公式のリポジトリにある手順に従って操作します。

検証環境

今回の検証は以下の環境で行いました。

Secrets Managerへのアクセス権限を付与

External Secretsは、Controllerが起動したPodから、外部サービスにアクセスしてデータを取得します。そのため、Podが対象のサービスにアクセスするためのアクセス権限を、クラスターまたはPodに付与する必要があります。ここではEKSクラスターのData Planeに対して、Secrets ManagerへのReadWrite権限を付与します。

eksctlコマンドを使用してEKSクラスターを作成すると、EKSを利用するために必要なIAMロールがデフォルトで付与されるので、AWSコンソール等から対象のロールに対してポリシーを追加することができます。

またEKSクラスター作成時にIAMポリシーを指定することも可能です。今回は以下のようなマニフェストファイルを指定してクラスターを作成しました。

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: eks-cluster
  region: ap-northeast-1
vpc:
  id: "vpc-xxxxxxxxxxxxxxxx"
  subnets:
    public:
      ap-northeast-1a: { id: subnet-xxxxxxxxxxxxxxxx }
      ap-northeast-1c: { id: subnet-xxxxxxxxxxxxxxxx }
      ap-northeast-1d: { id: subnet-xxxxxxxxxxxxxxxx }


nodeGroups:
- name: eks-nodegroup-1
  instanceType: t2.medium
  desiredCapacity: 2
  minSize: 2
  maxSize: 5
  securityGroups:
    attachIDs: [sg-xxxxxxxxxxxxxxxx]
  targetGroupARNs: 
    - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
    - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
    - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
    - arn:aws:iam::aws:policy/SecretsManagerReadWrite  # 新規追加

External Secretsのデプロイ

続いてExternal Secretsをデプロイします。デプロイはKubernetesのパッケージ管理ツールであるHelmを利用します。今回はhelm templateを利用して必要なマニフェストファイルを生成し、それらを利用することでデプロイを行いました。

$ git clone https://github.com/external-secrets/kubernetes-external-secrets.git
$ cd kubernetes-external-secrets
$ helm template --output-dir ./testdir ./charts/kubernetes-external-secrets/
wrote ./testdir/kubernetes-external-secrets/templates/serviceaccount.yaml
wrote ./testdir/kubernetes-external-secrets/templates/rbac.yaml
wrote ./testdir/kubernetes-external-secrets/templates/rbac.yaml
wrote ./testdir/kubernetes-external-secrets/templates/rbac.yaml
wrote ./testdir/kubernetes-external-secrets/templates/service.yaml
wrote ./testdir/kubernetes-external-secrets/templates/deployment.yaml

生成されたマニフェストファイルは以下の通りです。Helmチャートのvalues.yamlで指定する必要のあるパラメータがRELEASE-NOTE-と表記されており、これを適宜修正しないとデプロイに失敗します。

$ kubectl apply -f ./testdir/kubernetes-external-secrets/templates/deployment.yaml

The Deployment "RELEASE-NAME-kubernetes-external-secrets" is invalid:
* metadata.name: Invalid value: "RELEASE-NAME-kubernetes-external-secrets": a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
* spec.template.spec.serviceAccountName: Invalid value: "RELEASE-NAME-kubernetes-external-secrets": a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')

修正は手動のほか、オプションを指定することで生成時に指定することも可能です。 ただ私が試した限り--generate-nameオプションを追加してもRELEASE-NOTE-が変更されなかったため、そちらは手動で修正しました(こちらのIssueと関連する?)

serviceaccount.yaml

---
# Source: kubernetes-external-secrets/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: RELEASE-NAME-kubernetes-external-secrets  # 要修正
  namespace: "default"
  labels:
    app.kubernetes.io/name: kubernetes-external-secrets
    helm.sh/chart: kubernetes-external-secrets-6.0.0
    app.kubernetes.io/instance: RELEASE-NAME  # 要修正
    app.kubernetes.io/managed-by: Helm

rbac.yaml

---
# Source: kubernetes-external-secrets/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: RELEASE-NAME-kubernetes-external-secrets  # 要修正
  labels:
    app.kubernetes.io/name: kubernetes-external-secrets
    helm.sh/chart: kubernetes-external-secrets-6.0.0
    app.kubernetes.io/instance: RELEASE-NAME  # 要修正
    app.kubernetes.io/managed-by: Helm
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["create", "update"]
  - apiGroups: [""]
    resources: ["namespaces"]
    verbs: ["get", "watch", "list"]
  - apiGroups: ["apiextensions.k8s.io"]
    resources: ["customresourcedefinitions"]
    resourceNames: ["externalsecrets.kubernetes-client.io"]
    verbs: ["get", "update"]
  - apiGroups: ["kubernetes-client.io"]
    resources: ["externalsecrets"]
    verbs: ["get", "watch", "list"]
  - apiGroups: ["kubernetes-client.io"]
    resources: ["externalsecrets/status"]
    verbs: ["get", "update"]
  - apiGroups: ["apiextensions.k8s.io"]
    resources: ["customresourcedefinitions"]
    verbs: ["create"]
---
# Source: kubernetes-external-secrets/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: RELEASE-NAME-kubernetes-external-secrets  # 要修正
  labels:
    app.kubernetes.io/name: kubernetes-external-secrets
    helm.sh/chart: kubernetes-external-secrets-6.0.0
    app.kubernetes.io/instance: RELEASE-NAME  # 要修正
    app.kubernetes.io/managed-by: Helm
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: RELEASE-NAME-kubernetes-external-secrets  # 要修正
subjects:
  - name: RELEASE-NAME-kubernetes-external-secrets  # 要修正
    namespace: "default"
    kind: ServiceAccount
---
# Source: kubernetes-external-secrets/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: RELEASE-NAME-kubernetes-external-secrets-auth  # 要修正
  labels:
    app.kubernetes.io/name: kubernetes-external-secrets
    helm.sh/chart: kubernetes-external-secrets-6.0.0
    app.kubernetes.io/instance: RELEASE-NAME  # 要修正
    app.kubernetes.io/managed-by: Helm
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:auth-delegator
subjects:
- name: RELEASE-NAME-kubernetes-external-secrets  # 要修正
  namespace: "default"
  kind: ServiceAccount

service.yaml

---
# Source: kubernetes-external-secrets/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: RELEASE-NAME-kubernetes-external-secrets  # 要修正
  namespace: "default"
  labels:
    app.kubernetes.io/name: kubernetes-external-secrets
    helm.sh/chart: kubernetes-external-secrets-6.0.0
    app.kubernetes.io/instance: RELEASE-NAME  # 要修正
    app.kubernetes.io/managed-by: Helm
spec:
  selector:
    app.kubernetes.io/name: kubernetes-external-secrets
  ports:
    - protocol: TCP
      port: 3001
      name: prometheus
      targetPort: prometheus

deployment.yaml

---
# Source: kubernetes-external-secrets/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: RELEASE-NAME-kubernetes-external-secrets  # 要修正
  namespace: "default"
  labels:
    app.kubernetes.io/name: kubernetes-external-secrets
    helm.sh/chart: kubernetes-external-secrets-6.0.0
    app.kubernetes.io/instance: RELEASE-NAME  # 要修正
    app.kubernetes.io/managed-by: Helm
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: kubernetes-external-secrets
      app.kubernetes.io/instance: RELEASE-NAME  # 要修正
  template:
    metadata:
      labels:
        app.kubernetes.io/name: kubernetes-external-secrets
        app.kubernetes.io/instance: RELEASE-NAME  # 要修正
    spec:
      serviceAccountName: RELEASE-NAME-kubernetes-external-secrets  # 要修正
      containers:
        - name: kubernetes-external-secrets
          image: "godaddy/kubernetes-external-secrets:6.0.0"
          ports:
          - name: prometheus
            containerPort: 3001
          imagePullPolicy: IfNotPresent
          resources:
            {}
          env:
          - name: "AWS_DEFAULT_REGION"
            value: "us-west-2"
          - name: "AWS_REGION"
            value: "us-west-2"
          - name: "LOG_LEVEL"
            value: "info"
          - name: "LOG_MESSAGE_KEY"
            value: "msg"
          - name: "METRICS_PORT"
            value: "3001"
          - name: "POLLER_INTERVAL_MILLISECONDS"
            value: "10000"
          - name: "VAULT_ADDR"
            value: "http://127.0.0.1:8200"
          # Params for env vars populated from k8s secrets
      securityContext:
        runAsNonRoot: true
# オプションの指定例
$ helm template --output-dir ./testdir ./charts/kubernetes-external-secrets/ --set env.AWS_REGION="ap-northeast-1"
wrote ./testdir/kubernetes-external-secrets/templates/serviceaccount.yaml
wrote ./testdir/kubernetes-external-secrets/templates/rbac.yaml
wrote ./testdir/kubernetes-external-secrets/templates/rbac.yaml
wrote ./testdir/kubernetes-external-secrets/templates/rbac.yaml
wrote ./testdir/kubernetes-external-secrets/templates/service.yaml
wrote ./testdir/kubernetes-external-secrets/templates/deployment.yaml

$ cat testdir/kubernetes-external-secrets/templates/deployment.yaml | grep -3 AWS_REGION
          env:
          - name: "AWS_DEFAULT_REGION"
            value: "us-west-2"
          - name: "AWS_REGION"
            value: "ap-northeast-1"
          - name: "LOG_LEVEL"
            value: "info"

※参考ドキュメント:

デプロイ後は以下のようにリソースが作成されます。

# External Secretsデプロイ
$ kubectl apply -f ./output_dir/kubernetes-external-secrets/templates/
deployment.apps/kubernetes-external-secrets created
clusterrole.rbac.authorization.k8s.io/kubernetes-external-secrets created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-external-secrets created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-external-secrets-auth created
service/kubernetes-external-secrets created
serviceaccount/kubernetes-external-secrets created

# デプロイ後の確認
$ kubectl get pods
NAME                                           READY   STATUS    RESTARTS   AGE
kubernetes-external-secrets-59b8db67c5-9scxm   1/1     Running   0          44s

$ kubectl get service
NAME                          TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
kubernetes                    ClusterIP   172.20.0.1       <none>        443/TCP    35h
kubernetes-external-secrets   ClusterIP   172.20.177.142   <none>        3001/TCP   76s

AWS Secrets Managerへの登録

続いてAWS Secrets Managerのほうにテストデータを登録します。ここではAWS CLIからデータを登録します。

# 秘匿データの設定
$ aws secretsmanager create-secret --name hello-service/password --secret-string "5678"

# 作成後の確認
$ aws secretsmanager get-secret-value --secret-id hello-service/password
{
    "ARN": "arn:aws:secretsmanager:ap-northeast-1:111111111111:secret:hello-service/password-YmCbrt",
    "Name": "hello-service/password",
    "VersionId": "a298c3ea-1e27-4e05-b379-a13856e6f8c8",
    "SecretString": "5678",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": "2020-11-17T13:53:59.130000+09:00"
}

ExternalSecretリソースの作成

ここまででExternal Secretsを利用する準備が整ったので、ExternalSecretリソースをデプロイします。今回は以下のマニフェストファイルを利用しました。

hello-service-es.yaml

apiVersion: kubernetes-client.io/v1
kind: ExternalSecret
metadata:
  name: hello-service
spec:
  backendType: secretsManager
  region: ap-northeast-1
  data:
    - key: hello-service/password
      name: password
  # optional: specify a template with any additional markup you would like added to the downstream Secret resource.
  # This template will be deep merged without mutating any existing fields. For example: you cannot override metadata.name.
  template:
    metadata:
      annotations:
        cat: cheese
      labels:
        dog: farfel

上記ファイルを用いてリソースを作成します。以下の通り、ExternalSecretリソースを作成することで(外部データを参照したのちに)Secretリソースが作成されます。

# ExternalSecretリソースの作成
$ kubectl apply -f hello-service-es.yaml
externalsecret.kubernetes-client.io/hello-service created


# 作成後の確認
$ kubectl get externalsecret
NAME            LAST SYNC   STATUS    AGE
hello-service   7s          SUCCESS   7s

$ kubectl describe externalsecret hello-service
Name:         hello-service
Namespace:    default
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"kubernetes-client.io/v1","kind":"ExternalSecret","metadata":{"annotations":{},"name":"hello-service","namespace":"default"}...
API Version:  kubernetes-client.io/v1
Kind:         ExternalSecret
Metadata:
  Creation Timestamp:  2020-11-20T11:25:20Z
  Generation:          1
  Resource Version:    410381
  Self Link:           /apis/kubernetes-client.io/v1/namespaces/default/externalsecrets/hello-service
  UID:                 181d2db7-8162-4758-bb60-3b08c3fc4219
Spec:
  Backend Type:  secretsManager
  Data:
    Key:   hello-service/password
    Name:  password
  Region:  ap-northeast-1
Status:
  Last Sync:            2020-11-20T11:27:01.865Z
  Observed Generation:  1
  Status:               SUCCESS
Events:                 <none>



# Secretリソースが作成される
$ kubectl get secret
NAME                                      TYPE                                  DATA   AGE
default-token-88vp5                       kubernetes.io/service-account-token   3      35h
hello-service                             Opaque                                1      15s
kubernetes-external-secrets-token-v7prp   kubernetes.io/service-account-token   3      14m


$ kubectl get secret hello-service -o yaml
apiVersion: v1
data:
  password: NTY3OA==  # “5678”
kind: Secret
metadata:
  creationTimestamp: "2020-11-20T11:25:20Z"
  name: hello-service
  namespace: default
  ownerReferences:
  - apiVersion: kubernetes-client.io/v1
    controller: true
    kind: ExternalSecret
    name: hello-service
    uid: 181d2db7-8162-4758-bb60-3b08c3fc4219
  resourceVersion: "410070"
  selfLink: /api/v1/namespaces/default/secrets/hello-service
  uid: 4975494d-a2ce-4395-a4bb-cc79c7af8d04
type: Opaque

IAM Role for Service Account (IRSA)を利用する場合に必要な設定

ここまでの手順では、EKSクラスターのData Planeに対して、AWS Secrets ManagerへのReadWrite権限を付与していました。この設定ではExternalSecret Controller以外のPodもSecrets Managerにアクセスすることができてしまうため、セキュリティ的に問題となる場合があります。

AWSではIAM Role for Service Account (IRSA)という機能があります。これはKubernetes上で利用するアカウントであるService Accountに対してIAMロールを付与し、Pod起動時に特定のService Accountを指定することで、Pod単位での権限付与を実現する機能です。

irsa-diagram

AWS IAMは外部IDプロバイダーと連携することで、外部IDに対してAWSリソースへのアクセス権を付与することができます。IRSAでは、EKS上でのOpenID Connect(OIDC)プロバイダーを作成・有効化し、これを利用します。

一方KubernetesではVolumeリソースのprojectedプラグインを利用して、Service Account Tokenを利用することができます。projected VolumeはKubernetes Volumeリソースで利用できるプラグインの種類の一つで、Secret / ConfigMap / downwardAPI / Service Accoutn Tokenの4種類のリソースを1か所のディレクトリに集約してマウントする機能を提供します。このトークンにAWSへのアクセス権を付与することで、特定のService AccountがAWSリソースへアクセスすることを許可します。

Amazon EKS上でExternal Secretsを利用する際も、このIRSAを利用することで、必要最小限の権限付与に抑え、セキュリティを向上することが期待できます。

External Secretsを利用する場合、ExternalSecrets Controller Podを作成すると、Amazon EKS Pod Identity Webhookを介して、Podに必要な環境変数を追加します。この環境変数にはIAMロールのARNとトークンの情報が含まれ、Podに対してprojectedプラグインのVolumeを設定します。

次にExternalSecretリソースを作成すると、PodがAWSリソースにアクセスしようとし、OIDCプロバイダーに対するトークンの検証をSTS経由で行います。認証に成功すればアクセス権を付与(ここではAWS Secrets Managerへのアクセス)し、AWS Secrets Managerに保管された秘匿データを取得、Secretリソースの作成を行うことができます。

※External SecretsをIRSAで利用する場合の簡略図。図中の数字は処理のステップ順を表す。

f:id:FY0323:20201218232106j:plain

IRSAを利用するには、以下のような操作が必要となります。なお、以降の作業は、AWS Secrets Managerへのアクセスを許可するIAMロールが無い状況を想定していますので、必要があればEKSクラスターから指定のロールの削除を実行します。

OpenID Connectプロバイダーの有効化

IRSAを利用するには、まずクラスター上でOpenID Connect (OIDC) IDプロバイダーを有効にする必要があります。IDプロバイダーを有効にするにはeksctl utils associate-iam-oidc-providerコマンドを実行します。

$ eksctl utils associate-iam-oidc-provider --name eks-cluster --approve
Flag --name has been deprecated, use --cluster
[]  eksctl version 0.29.2
[]  using region ap-northeast-1
[]  will create IAM Open ID Connect provider for cluster "eks-cluster" in "ap-northeast-1"
[]  created IAM Open ID Connect provider for cluster "eks-cluster" in "ap-northeast-1"

# 有効化後の確認
$ aws iam list-open-id-connect-providers
{
    "OpenIDConnectProviderList": [
        {
            "Arn": "arn:aws:iam::111111111111:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/11111111111111111111111111111111"
        }
    ]
}

IAMロールの作成

次にService Accountに付与するIAMロールを用意します。今回はAWS Secrets Managerへアクセスするために、以下のようなCloudFormationテンプレートを利用してIAMロールを作成します。

AWSTemplateFormatVersion: 2010-09-09
Resources:
  eksirsa:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Federated: 
              - arn:aws:iam::111111111111:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/11111111111111111111111111111111
            Action:
              - 'sts:AssumeRoleWithWebIdentity'
            Condition:
              StringEquals:         
                oidc.eks.ap-northeast-1.amazonaws.com/id/11111111111111111111111111111111:sub: system:serviceaccount:default:kubernetes-external-secrets
                oidc.eks.ap-northeast-1.amazonaws.com/id/11111111111111111111111111111111:aud: sts.amazonaws.com
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/SecretsManagerReadWrite
      RoleName: eks-irsa
      Tags:
        - Key: alpha.eksctl.io/cluster-name
          Value: eks-cluster
        - Key: eksctl.cluster.k8s.io/v1alpha1/cluster-name
          Value: eks-cluster
        - Key: alpha.eksctl.io/iamserviceaccount-name
          Value: default/kubernetes-external-secrets
        - Key: alpha.eksctl.io/eksctl-version
          Value: 0.29.2
# エラーが発生する場合は”--capabilities CAPABILITY_NAMED_IAM”を追加
$ aws cloudformation deploy --stack-name eks-irsa --template aws-iam-irsa.yaml --capabilities CAPABILITY_NAMED_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - eks-irsa

# 作成後の確認
$ aws iam get-role --role-name eks-irsa
{
    "Role": {
        "Path": "/",
        "RoleName": "eks-irsa",
        "RoleId": "AROATYRF5FWUEKGK4AIKJ",
        "Arn": "arn:aws:iam::111111111111:role/eks-irsa",
        "CreateDate": "2020-12-12T23:05:21+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Federated": "arn:aws:iam::111111111111:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/11111111111111111111111111111111"
                    },
                    "Action": "sts:AssumeRoleWithWebIdentity",
                    "Condition": {
                        "StringEquals": {
                            "oidc.eks.ap-northeast-1.amazonaws.com/id/11111111111111111111111111111111:aud": "sts.amazonaws.com",
                            "oidc.eks.ap-northeast-1.amazonaws.com/id/11111111111111111111111111111111:sub": "system:serviceaccount:default:kubernetes-external-secrets"
                        }
                    }
                }
            ]
        },
        "Description": "",
        "MaxSessionDuration": 3600,
        "Tags": [
            {
                "Key": "alpha.eksctl.io/cluster-name",
                "Value": "eks-cluster"
            },
            {
                "Key": "eksctl.cluster.k8s.io/v1alpha1/cluster-name",
                "Value": "eks-cluster"
            },
            {
                "Key": "alpha.eksctl.io/iamserviceaccount-name",
                "Value": "default/kubernetes-external-secrets"
            },
            {
                "Key": "alpha.eksctl.io/eksctl-version",
                "Value": "0.29.2"
            }
        ],
        "RoleLastUsed": {}
    }
}

Service Accountに設定を追加

次に、作成したIAMロールをService Accountで利用するよう、設定を変更します。

---
# Source: kubernetes-external-secrets/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kubernetes-external-secrets
  namespace: "default"
  labels:
    app.kubernetes.io/name: kubernetes-external-secrets
    helm.sh/chart: kubernetes-external-secrets-6.0.0
    app.kubernetes.io/instance: 6.0.0
    app.kubernetes.io/managed-by: Helm
  # 以降の記述を追加
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::111111111111:role/eks-irsa
# Service Account更新
$ kubectl apply -f output_dir/kubernetes-external-secrets/templates/serviceaccount.yaml
serviceaccount/kubernetes-external-secrets configured


# 更新後はAnnotationsが付与されている
$ kubectl get sa
NAME                          SECRETS   AGE
default                       1         6d22h
kubernetes-external-secrets   1         6d21h

$ kubectl describe sa kubernetes-external-secrets
Name:                kubernetes-external-secrets
Namespace:           default
Labels:              app.kubernetes.io/instance=6.0.0
                     app.kubernetes.io/managed-by=Helm
                     app.kubernetes.io/name=kubernetes-external-secrets
                     helm.sh/chart=kubernetes-external-secrets-6.0.0
Annotations:         eks.amazonaws.com/role-arn: arn:aws:iam::111111111111:role/eks-irsa
                     kubectl.kubernetes.io/last-applied-configuration:
                       {"apiVersion":"v1","kind":"ServiceAccount","metadata":{"annotations":{"eks.amazonaws.com/role-arn":"arn:aws:iam::111111111111:role/eks-irs...
Image pull secrets:  <none>
Mountable secrets:   kubernetes-external-secrets-token-wggsc
Tokens:              kubernetes-external-secrets-token-wggsc
Events:              <none>

Deploymentリソースの設定変更

これでIAMロールを付与したService Accountを利用できるようになったので、Deploymentなどのリソースを作成します。

Deploymentリソース作成後に内容を確認すると、aws-iam-tokenという名称のProjected Volumeが付与された様子が確認できます。

# Deployment再作成
$ kubectl apply -f output_dir/kubernetes-external-secrets/templates/deployment.yaml
deployment.apps/kubernetes-external-secrets configured


# 作成後の確認
$ kubectl get pods
NAME                                           READY   STATUS        RESTARTS   AGE
kubernetes-external-secrets-59859579b6-xqkg4   1/1     Running       0          7s
kubernetes-external-secrets-59b8db67c5-r74k8   0/1     Terminating   0          6d21h


$ kubectl describe pod kubernetes-external-secrets-59859579b6-xqkg4
Name:         kubernetes-external-secrets-59859579b6-xqkg4
Namespace:    default
Priority:     0
Node:         ip-10-0-3-64.ap-northeast-1.compute.internal/10.0.3.64
Start Time:   Sun, 13 Dec 2020 08:10:52 +0900
Labels:       app.kubernetes.io/instance=6.0.0
              app.kubernetes.io/name=kubernetes-external-secrets
              pod-template-hash=59859579b6
Annotations:  kubernetes.io/psp: eks.privileged
Status:       Running
IP:           10.0.3.247
IPs:
  IP:           10.0.3.247
Controlled By:  ReplicaSet/kubernetes-external-secrets-59859579b6
Containers:
  kubernetes-external-secrets:
    Container ID:   docker://f6813c88c87e065be47fa29b1de1eb2434928eeb30f866137034212832d8bdf2
    Image:          godaddy/kubernetes-external-secrets:6.0.0
    Image ID:       docker-pullable://godaddy/kubernetes-external-secrets@sha256:e827f7bc272117a97a8562ac70e37810266467f07d2fce798c8abc360b892c08
    Port:           3001/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Sun, 13 Dec 2020 08:10:53 +0900
    Ready:          True
    Restart Count:  0
    Environment:
      AWS_DEFAULT_REGION:            ap-northeast-1
      AWS_REGION:                    ap-northeast-1
      LOG_LEVEL:                     info
      LOG_MESSAGE_KEY:               msg
      METRICS_PORT:                  3001
      POLLER_INTERVAL_MILLISECONDS:  10000
      VAULT_ADDR:                    http://127.0.0.1:8200
      AWS_ROLE_ARN:                  arn:aws:iam::111111111111:role/eks-irsa
      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 kubernetes-external-secrets-token-wggsc (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  aws-iam-token:  # 追加されている
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  86400
  kubernetes-external-secrets-token-wggsc:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  kubernetes-external-secrets-token-wggsc
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age   From                                                   Message
  ----    ------     ----  ----                                                   -------
  Normal  Scheduled  3s    default-scheduler                                      Successfully assigned default/kubernetes-external-secrets-59859579b6-xqkg4 to ip-10-0-3-64.ap-northeast-1.compute.internal
  Normal  Pulled     2s    kubelet, ip-10-0-3-64.ap-northeast-1.compute.internal  Container image "godaddy/kubernetes-external-secrets:6.0.0" already present on machine
  Normal  Created    2s    kubelet, ip-10-0-3-64.ap-northeast-1.compute.internal  Created container kubernetes-external-secrets
  Normal  Started    2s    kubelet, ip-10-0-3-64.ap-northeast-1.compute.internal  Started container kubernetes-external-secrets

これでExternalSecretリソースが作成できるかと思いましたが、この状態でリソースを作成しようとすると、以下のようなエラーが発生し、Secrets Managerからデータを取得することができませんでした。

$ kubectl apply -f hello-service-es.yaml
externalsecret.kubernetes-client.io/hello-service created

$ kubectl get externalsecret
NAME            LAST SYNC   STATUS
                                                                                                                                                 AGE
hello-service   5s          ERROR, User: arn:aws:sts::111111111111:assumed-role/eksctl-eks-cluster-nodegroup-eks-NodeInstanceRole-1R5YXUU3GJMP5/i-09defe966bee6cb58 is not authorized
to perform: secretsmanager:GetSecretValue on resource: arn:aws:secretsmanager:ap-northeast-1:111111111111:secret:hello-service/password-YmCbrt   6s

エラーメッセージを見る限り、EKSクラスターのNodeGroupに対して付与されるIAMロールを利用しようとしているように見えます。ExternalSecretリソースの定義では、利用するARNを指定することもできるため、こちらを変更してみます。

apiVersion: kubernetes-client.io/v1
kind: ExternalSecret
metadata:
  name: hello-service
spec:
  backendType: secretsManager
  # optional: specify role to assume when retrieving the data
  roleArn: arn:aws:iam::111111111111:role/eks-irsa
  region: ap-northeast-1
  data:
    - key: hello-service/password
      name: password
  # optional: specify a template with any additional markup you would like added to the downstream Secret resource.
  # This template will be deep merged without mutating any existing fields. For example: you cannot override metadata.name.
  template:
    metadata:
      annotations:
        cat: cheese
      labels:
        dog: farfel

上記ファイルを用いてリソースを作成すると、メッセージは変更されたものの、やはりエラーメッセージが確認できます。

# 作成済みのリソースを一度削除する
$ kubectl delete -f hello-service-es.yaml
externalsecret.kubernetes-client.io "hello-service" deleted


# 再作成
$ kubectl apply -f hello-service-es.yaml
externalsecret.kubernetes-client.io/hello-service created


# 作成後の確認
$ kubectl get externalsecret
NAME            LAST SYNC   STATUS                                 AGE
hello-service   4s          ERROR, Missing credentials in config   4s

いろいろと調べてみたところ、External SecretsでIRSAを利用するには、DeploymentのSecurity Cntextの設定を変更し、fsGroupを以下のように変更する必要があるようです。

(中略)

securityContext:
  runAsNonRoot: true
  fsGroup: 65534

こちらの情報は、現在一緒にお仕事をさせて頂いている方から教えて頂きました。この場を借りて感謝申し上げます。

※参考リンク:

上記設定を追加したところ、無事にデータを取得し、Secretリソースが作成されることを確認できました。

# 設定変更後、Controllerを再作成

$ kubectl apply -f output_dir/kubernetes-external-secrets/templates/deployment.yaml
deployment.apps/kubernetes-external-secrets created


# 作成後の確認
$ kubectl get deployment kubernetes-external-secrets -o yaml

(中略)

      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext:
        fsGroup: 65534  ★
        runAsNonRoot: true
      serviceAccount: kubernetes-external-secrets
      serviceAccountName: kubernetes-external-secrets
      terminationGracePeriodSeconds: 30

(中略)



# ExternalSecretリソースの作成
$ kubectl apply -f hello-service-es.yaml
externalsecret.kubernetes-client.io/hello-service created


# 作成後の確認
$ kubectl get externalsecret
NAME            LAST SYNC   STATUS    AGE
hello-service   5s          SUCCESS   8s

$ kubectl get secret
NAME                                      TYPE                                  DATA   AGE
default-token-875qc                       kubernetes.io/service-account-token   3      6d22h
hello-service                             Opaque                                1      14s
kubernetes-external-secrets-token-wggsc   kubernetes.io/service-account-token   3      6d21h

$ kubectl get secret hello-service -o yaml
apiVersion: v1
data:
  password: NTY3OA==  # “5678”
kind: Secret
metadata:
  creationTimestamp: "2020-12-12T23:27:03Z"
  name: hello-service
  namespace: default
  ownerReferences:
  - apiVersion: kubernetes-client.io/v1
    controller: true
    kind: ExternalSecret
    name: hello-service
    uid: 44e6b1f7-3c91-4a7c-b316-a2cec040a114
  resourceVersion: "1762644"
  selfLink: /api/v1/namespaces/default/secrets/hello-service
  uid: 34b88216-3814-4d32-873c-f10103504d75
type: Opaque

参考ドキュメント