Install External Secrets Operator with CDK and make Secrets Manager data available as Kubernetes Secret

kubernetesaws

External Secrets Operator (ESO) is an Operator that creates Kubernetes Secrets from secrets stored in external services, and is the successor to the deprecated Kubernetes External Secrets (KES). In this article, I install it with CDK and create a Secret that contains data from Secrets Manager.

First, create an IAM Role to be granted to a ServiceAccount and install the operator with Helm Chart.

const externalSecretsRole = new iam.Role(this, 'ExternalSecretsRole', {
  roleName: `ExternalSecretsRole-${cluster.clusterName}`,
  assumedBy: new iam.WebIdentityPrincipal(
    cluster.openIdConnectProvider.openIdConnectProviderArn,
    {
      "StringEquals": new cdk.CfnJson(this, 'SecretStoreSecretsmanagerRoleStringEquals', { value: {
          [`${cluster.clusterOpenIdConnectIssuer}:aud`]: "sts.amazonaws.com",
          [`${cluster.clusterOpenIdConnectIssuer}:sub`]: `system:serviceaccount:external-secrets:secret-store-sa`
        }})
    }
  ),
  inlinePolicies: {
    'SecretsAccess': new iam.PolicyDocument({
      statements: [
        new iam.PolicyStatement({
          actions: [
            "secretsmanager:GetSecretValue",
          ],
          resources: [
            `arn:${cdk.Aws.PARTITION}:secretsmanager:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:secret:some-secret-*`
          ]
        })
      ]
    })
  }
})

const externalSecrets = cluster.addHelmChart('ExternalSecrets', {
  release: 'external-secrets',
  chart: 'external-secrets',
  repository: 'https://charts.external-secrets.io',
  version: '0.9.9',
  namespace: 'external-secrets',
  createNamespace: true,
  values: {
    serviceAccount: {
      name: "secret-store-sa",
      annotations: {
        "eks.amazonaws.com/role-arn": externalSecretsRole.roleArn
      }
    },
  },
  wait: true,
})

Next, create a SecretStore for SecretsManager. I create a ClusterSecretStore to use it from multiple namespaces.

const secretStoreSecretsManager = cluster.addManifest('SecretStoreSecretsmanager', {
  apiVersion: 'external-secrets.io/v1beta1',
  kind: 'ClusterSecretStore',
  metadata: {
    name: 'secretsmanager',
  },
  spec: {
    provider: {
      aws: {
        service: 'SecretsManager',
        region: 'us-east-1',
      }
    }
  }
})

secretStoreSecretsManager.node.addDependency(externalSecrets)

Finally, create an ExternalSecret to read the Secret from SecretsManager.

const someExternalSecret = cluster.addManifest('SomeExternalSecret', {
  apiVersion: 'external-secrets.io/v1beta1',
  kind: 'ExternalSecret',
  metadata: {
    name: `some-external-secret`,
  },
  spec: {
    refreshInterval: '1h',
    secretStoreRef: {
      name: 'secretsmanager',
      kind: 'ClusterSecretStore'
    },
    data: [{
      secretKey:  'kubernetes-secret-key',
      remoteRef: {
        key: secretName,
        property: 'secrets-manager-field-name' // {"secrets-manager-field-name": "xxxx"}
      }
    }]
  }
})

someExternalSecret.node.addDependency(secretStoreSecretsManager)

As a result, the following Secret is created. If it cannot be retrieved, the stack will be rolled back, so it takes a long time if you try to create the cluster and the Secret at the same time. You would be better to create them one by one first.

$ kubectl describe secret some-external-secret
Name:         some-external-secret
Namespace:    default
...
Type:  Opaque

Data
====
kubernetes-secret-key:  4 bytes

参考

EKSにおけるkubernetes-external-secretsとIRSAによる権限移譲 - decadence