[小ネタ] AWS CDKでEKSを作成する際に、 aws-auth も作成する方法

はじめに

おはようございます、加藤です。先日、AWS CDK(以降、CDK)にアップデートがあり、Amazon Elastic Kubernetes Service(以降、EKS)を作成する際にKubernetes(以降、k8s)リソースも作成が可能になりました。

Release v1.4.0 · aws/aws-cdk

試しにCloudFormation(以降、CFn)を生成してみると、k8sリソースがカスタムリソースを使っている事がわかりました。

  AwsAuthmanifest00905E16:
    Type: Custom::AWSCDK-EKS-KubernetesResource
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - EksKubernetesResourceHandlerC6DECF98
          - Arn
      Manifest:
        Fn::Join:
          - ""
          - - '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\"rolearn\":\"'
            - Fn::GetAtt:
                - EksDefaultCapacityInstanceRole2307F75A
                - Arn
            - '\",\"username\":\"system:node:{{EC2PrivateDNSName}}\",\"groups\":[\"system:bootstrappers\",\"system:nodes\"]},{\"rolearn\":\"arn:aws:iam::'
            - Ref: AWS::AccountId
            - ':role/cm-001\",\"username\":\"001\",\"groups\":[\"system:masters\"]},{\"rolearn\":\"arn:aws:iam::'
            - Ref: AWS::AccountId
            - ':role/cm-002\",\"username\":\"002\",\"groups\":[\"system:masters\"]},{\"rolearn\":\"arn:aws:iam::'
            - Ref: AWS::AccountId
            - ':role/cm-003\",\"username\":\"003\",\"groups\":[\"system:masters\"]},{\"rolearn\":\"arn:aws:iam::'
            - Ref: AWS::AccountId
            - :role/cm-004\",\"username\":\"004\",\"groups\":[\"system:masters\"]}]","mapUsers":"[]","mapAccounts":"[]"}}]
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: EksStack/AwsAuth/manifest/Resource/Default
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Modules: aws-cdk=1.4.0,@aws-cdk/assets=1.4.0,@aws-cdk/aws-autoscaling=1.4.0,@aws-cdk/aws-autoscaling-common=1.4.0,@aws-cdk/aws-cloudformation=1.4.0,@aws-cdk/aws-cloudwatch=1.4.0,@aws-cdk/aws-ec2=1.4.0,@aws-cdk/aws-eks=1.4.0,@aws-cdk/aws-elasticloadbalancingv2=1.4.0,@aws-cdk/aws-events=1.4.0,@aws-cdk/aws-iam=1.4.0,@aws-cdk/aws-kms=1.4.0,@aws-cdk/aws-lambda=1.4.0,@aws-cdk/aws-s3=1.4.0,@aws-cdk/aws-s3-assets=1.4.0,@aws-cdk/aws-sqs=1.4.0,@aws-cdk/aws-ssm=1.4.0,@aws-cdk/core=1.4.0,@aws-cdk/cx-api=1.4.0,@aws-cdk/region-info=1.4.0,jsii-runtime=node.js/v10.16.0

以前、EKSクラスターは、Type: AWS::EKS::Clusterで作成されていましたが、こちらもカスタムリソースで作成されています。
カスタムリソースを使用したくない場合は、クラスターのプロパティでkubectlEnabled: falseを設定する事で対応が可能です。(この場合は、CDKでk8sリソースの作成が出来なくなります)

  EksBC5472D2:
    Type: Custom::AWSCDK-EKS-Cluster
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - EksResourceHandler52D8126A
          - Arn
      Config:
        roleArn:
          Fn::GetAtt:
            - EksClusterRole457AF3D0
            - Arn
        version: "1.13"
        resourcesVpcConfig:
          securityGroupIds:
            - Fn::GetAtt:
                - EksControlPlaneSecurityGroupDC02C823
                - GroupId
          subnetIds:
            - Fn::ImportValue: NetworkStack:ExportsOutputRefVPCPublicSubnet1SubnetB4246D30D84F935B
            - Fn::ImportValue: NetworkStack:ExportsOutputRefVPCPublicSubnet2Subnet74179F3969CC10AD
            - Fn::ImportValue: NetworkStack:ExportsOutputRefVPCPrivateSubnet1Subnet8BCA10E01F79A1B7
            - Fn::ImportValue: NetworkStack:ExportsOutputRefVPCPrivateSubnet2SubnetCFCDAA7AB22CF85D
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: EksStack/Eks/Resource/Resource/Default

何ができるのか

CDKでk8sリソースを定義する事が可能になります。具体的には下記のように、Podなどのk8sリソースをCDKから作成できます。

const cluster = new eks.Cluster(this, 'hello-eks');

cluster.addResource('mypod', {
  apiVersion: 'v1',
  kind: 'Pod',
  metadata: { name: 'mypod' },
  spec: {
    containers: [
      {
        name: 'hello',
        image: 'paulbouwer/hello-kubernetes:1.5',
        ports: [ { containerPort: 8080 } ]
      }
    ]
  }
});

aws-eks module · AWS CDK

とはいえ、CDKでk8sリソースが出来る事が役立つシチュエーションは極めて限定的であると考えています。
タイトルの通り、思いつくのは aws-auth でしょう。EKSクラスターは作成直後、作成したIAMユーザー/ロールからのみアクセスが可能です。今回の場合ではカスタムリソース=AWS Lambdaが作成を行っているので、Lambda関数に割り当てられたIAMロールのみがデフォルトではアクセス権限を持ちます。(kubectlEnabled: falseの場合は、cdk deployを実行したIAMユーザー/ロールがアクセス権限を持ちます)
ワーカーノードも例外ではなく、作成直後はノードステータスが NotReady となっています。(EKSはコントロールプレーンのサービスで直接EC2と連携しないので、当然といえば当然ですが...)

aws-auth

有り難い事に、aws-auth専用のクラスが用意されており、簡単に記載する事ができます。

    // aws-auth settings
    const awsAuth = new AwsAuth(this, "AwsAuth", {
      cluster: this.cluster
    });
    awsAuth.addRoleMapping(asg.role, {
      groups: ["system:bootstrappers", "system:nodes"],
      username: "system:node:{{EC2PrivateDNSName}}"
    });
    const users = ["001", "002", "003", "004"];
    for (let i = 0; i < users.length; i++) {
      awsAuth.addMastersRole(
        Role.fromRoleArn(
          this,
          `${users[i]}`,
          `arn:aws:iam::${this.account}:role/cm-${users[i]}`
        ),
        `${users[i]}`
      );
    }

参考として、EKSスタック部分のコードを記載しておきます。

import { Construct, Stack, StackProps, CfnOutput } from "@aws-cdk/core";
import { Cluster, AwsAuth } from "@aws-cdk/aws-eks";
import {
  InstanceClass,
  InstanceSize,
  InstanceType,
  IVpc
} from "@aws-cdk/aws-ec2";
import { ManagedPolicy, Role } from "@aws-cdk/aws-iam";

interface EksStackProps extends StackProps {
  vpc: IVpc;
}

export class EksStack extends Stack {
  public readonly cluster: Cluster;

  constructor(scope: Construct, id: string, props: EksStackProps) {
    super(scope, id, props);
    this.cluster = new Cluster(this, "Eks", {
      vpc: props.vpc,
      version: "1.13",
      defaultCapacity: 0
    });

    // default worker nodes setting
    const asg = this.cluster.addCapacity("DefaultCapacity", {
      desiredCapacity: 2,
      instanceType: InstanceType.of(InstanceClass.M5, InstanceSize.LARGE),
      mapRole: false
    });
    asg.addUserData(
      "cd /tmp\n" +
        "yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm\n" +
        "systemctl start amazon-ssm-agent"
    );
    const managedPolicyNames = [
      "service-role/AmazonEC2RoleforSSM",
      "ElasticLoadBalancingFullAccess",
      "AutoScalingFullAccess"
    ];
    for (let managedPolicyName of managedPolicyNames) {
      asg.role.addManagedPolicy(
        ManagedPolicy.fromAwsManagedPolicyName(managedPolicyName)
      );
    }

    // aws-auth settings
    const awsAuth = new AwsAuth(this, "AwsAuth", {
      cluster: this.cluster
    });
    awsAuth.addRoleMapping(asg.role, {
      groups: ["system:bootstrappers", "system:nodes"],
      username: "system:node:{{EC2PrivateDNSName}}"
    });
    const users = ["001", "002", "003", "004"];
    for (let i = 0; i < users.length; i++) {
      awsAuth.addMastersRole(
        Role.fromRoleArn(
          this,
          `${users[i]}`,
          `arn:aws:iam::${this.account}:role/cm-${users[i]}`
        ),
        `${users[i]}`
      );
    }

    // Output
    new CfnOutput(this, "EksClusterName", {
      value: this.cluster.clusterName
    });
  }
}

注意事項

CDK 1.4.0では、cdk diffを行った際に必ず差分が検知されてしまいます。既にissuesが上がっていますが、差分が無い時〇〇するというような処理を組み込んでCI/CDしている場合は特に注意が必要です。

Stack NetworkStack
There were no differences
Stack EksStack
The EksStack stack uses assets, which are currently not accounted for in the diff output! See https://github.com/aws/aws-cdk/issues/395
Template
[+] Transform Transform: AWS::Serverless-2016-10-31

Resources
[~] AWS::Serverless::Application kubectl-layer-8C2542BC-BF2B-4DFE-B765-E181FD30A9A0 kubectllayer8C2542BCBF2B4DFEB765E181FD30A9A0617C4ADA replace

"cdk diff" should take assets into account · Issue #395 · aws/aws-cdk

あとがき

紹介しておいて、なんですがCDKでk8sリソースを作成するか否かは十分に検討する必要があると考えています。
k8sを使用する際は、Argo CDSpinnakerなどの、CDツールを利用する事が多いと思われます。そういった、CDツールがある前提でk8sリソースの設定を分散させる事は管理を複雑にします。
今後、EKS環境をCDK&Argo CDで構築する予定ですが、どういった構成にするかは構築を進めながら考えて行く予定です。