はじめに
この記事は『クラウドインテグレーション事業部SRE第三セクションブログリレー企画』の3回目(全6回)の記事です。
前回は大嵩さんの「初めてTerraform importしてみた」が公開されました。
EKS クラスターを構築するには eksctl や Terraform を用いるのが一般的かと思いますが
CDK で EKS クラスターを構築してみて、想像以上に便利だったので構築手順を紹介します。
作成する構成は以下の通りで、Fargate に Nginx のデプロイを行い、
ALB Controller にて、ALB からアクセスできる状態のものを構築します。
CDKプロジェクトの作成
今回は TypeScript を用いて CDK のプロジェクトを作成します。
はじめに、公式チュートリアルの手順を参考にプロジェクトを作成してきます。
$ mkdir hello-eks && cd hello-eks $ cdk init app --language typescript
EKSクラスター構築について
CDK でのクラスター構築は kubectlLayer というクラスを用いて作成される Lambda が構築を行います。
この Lambda を用いることで、 CDK ではマニフェストの適応や Helm Chart のインストールなどといった EKS の操作が可能になります。
この辺りの経緯は以下の記事の解説がわかりやすかったです。
https://qiita.com/watany/items/daf433338de5b6858ed6
kubectlLayer を使用するには別途ライブラリが必要になるためインストールを行います。
現在の EKS の最新バージョンは 1.31 のため以下のバージョンのものをインストールしましょう。
https://www.npmjs.com/package/@aws-cdk/lambda-layer-kubectl-v31
$ npm i @aws-cdk/lambda-layer-kubectl-v31
構築
作成するCDKプロジェクトのディレクトリ構成は以下のように、各リソースは resources ディレクトリ配下で管理を行い、stack ディレクトリ配下でスタックの作成する構成にしています。
hello-eks/ └─ lib ├─ resources │ ├─ aws │ │ ├─ eks.ts │ │ └─ network.ts │ └─ k8s │ └─ manifest.ts └─ stack └─ eks-stack.ts
Resource
まずはじめに、作成する AWS リソースと k8s のマニフェストを定義を行います。
VPC
簡単に VPC と 2AZ にパブリックサブネット・プライベートサブネットをそれぞれ1つずつ作成するコードを記述しました。
# lib/resources/aws/network.ts import { IpAddresses, IVpc, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2"; import { Construct } from "constructs"; export class Network { private readonly envName: string; private readonly projectName: string; private readonly scope: Construct; private readonly manageNumber: string; public vpc: IVpc; constructor(args: { scope: Construct; manageNumber: string; projectName?: string; envName?: string; }) { this.projectName = args.projectName || "hello-eks"; this.envName = args.envName || "dev"; this.scope = args.scope; this.manageNumber = args.manageNumber; } public createVpc(args: { networkCidr: string }) { const baseName = `${this.projectName}-${this.envName}`; const vpcName = `${baseName}-vpc-${this.manageNumber}`; this.vpc = new Vpc(this.scope, vpcName, { vpcName, ipAddresses: IpAddresses.cidr(args.networkCidr), createInternetGateway: true, enableDnsHostnames: true, enableDnsSupport: true, maxAzs: 2, natGateways: 1, restrictDefaultSecurityGroup: true, subnetConfiguration: [ { name: `${baseName}-public-subnet`, cidrMask: 24, subnetType: SubnetType.PUBLIC, }, { name: `${baseName}-private-subnet`, cidrMask: 24, subnetType: SubnetType.PRIVATE_WITH_EGRESS, }, ], }); } }
EKSクラスター
今回の実装する上で最低限必要となるもののみ定義をしています。
CDK にて EKS クラスターを構築する上でのポイントは以下になります。
- 権限まわり
EKS クラスター構築後は作成者のみが system:masters の権限を持っていますが、CDK で作成する場合はこれは cdk deploy を実行するユーザーではなく、Lambda がこの権限を持ちます。
そのため、IAM ユーザーに対して権限を付与する場合は別途 AccessEntry クラスを呼び出す必要があります。 - ワーカーノードの設定
CDK では Cluster クラスにて defaultCapacity や defaultCapacityInstance などのプロパティを設定しないと、デフォルトで m5.large の EC2 を2つ持つマネージドノードが作成されます。
そのため、Fargate を設定する場合や、細かい部分までマネージドノードの設定を行いたい場合などは defaultCapacity を 0 に設定し、別途 ワーカーノードのメソッド等を呼び出す必要があります。
# lib/resources/aws/eks.ts import { IVpc, SubnetSelection } from "aws-cdk-lib/aws-ec2"; import { AccessEntry, AccessPolicy, AccessScopeType, AlbController, AlbControllerVersion, AuthenticationMode, CfnAddon, Cluster, FargateProfile, KubernetesVersion, Selector, } from "aws-cdk-lib/aws-eks"; import { ILayerVersion } from "aws-cdk-lib/aws-lambda"; import { Construct } from "constructs"; export class EKSCluster { private readonly envName: string; private readonly projectName: string; public scope: Construct; public cluster: Cluster; private manageNumber: string; constructor(args: { scope: Construct; manageNumber: string; projectName?: string; envName?: string; }) { this.projectName = args.projectName || "hello-eks"; this.envName = args.envName || "dev"; this.scope = args.scope; this.manageNumber = args.manageNumber; } public createCluster(args: { vpc: IVpc; version: KubernetesVersion; kubectlLayer: ILayerVersion; }) { const clusterName = `${this.projectName}-${this.envName}-cluster-${this.manageNumber}`; this.cluster = new Cluster(this.scope, clusterName, { clusterName, version: args.version, kubectlLayer: args.kubectlLayer, vpc: args.vpc, defaultCapacity: 0, authenticationMode: AuthenticationMode.API_AND_CONFIG_MAP, }); } public createAddon(args: { addonName: string; addonVersion: string; role?: string; }) { new CfnAddon(this.scope, args.addonName, { clusterName: this.cluster.clusterName, addonName: args.addonName, addonVersion: args.addonVersion, ...(args.role && { serviceAccountRoleArn: args.role }), }); } public createAlbController(args: { version: AlbControllerVersion }) { new AlbController(this.scope, "alb-controller", { cluster: this.cluster, version: args.version, }); } public createFargateProfile(args: { vpc: IVpc; subnet: SubnetSelection; selectors: Selector[]; }) { new FargateProfile( this.scope, `${this.projectName}-${this.envName}-common-fargate-profile-${this.manageNumber}`, { cluster: this.cluster, vpc: args.vpc, subnetSelection: args.subnet, selectors: args.selectors, } ); } public createAccessEntry(args: { iamArn: string; accessPolicyName: | "AmazonEKSAdminPolicy" | "AmazonEKSClusterAdminPolicy" | "AmazonEKSAdminViewPolicy" | "AmazonEKSEditPolicy" | "AmazonEKSViewPolicy"; accessScopeType: AccessScopeType; manageNumber: string; }) { new AccessEntry(this.scope, `access-policy-${args.manageNumber}`, { cluster: this.cluster, principal: args.iamArn, accessPolicies: [ AccessPolicy.fromAccessPolicyName(args.accessPolicyName, { accessScopeType: args.accessScopeType, }), ], }); } }
マニフェスト
仮で作ったもののため、汎用性は高くありませんが、検証のため以下のようなマニフェストを用意しました。
呼び出すことで Namespce, Deployment, Ingress のリソースを作成できます。
今回はこちらを用いて Nginx のリソースを作成します。
# lib/resources/k8s/manifest.ts import { Cluster, KubernetesManifest } from "aws-cdk-lib/aws-eks"; export class Manifest { private readonly cluster: Cluster; private appName: string; private namespace: KubernetesManifest; private deployment: KubernetesManifest; private service: KubernetesManifest; private ingress: KubernetesManifest; constructor(args: { cluster: Cluster; appName: string }) { this.cluster = args.cluster; this.appName = args.appName; } public createService(args: { namespace: string; replicas: number; imageRepo: string; protocol: "TCP" | "UDP"; port: number; targetPort: number; scheme: "internet-facing" | "internal"; targetType: "ip" | "instance"; pathType: "Exact" | "Prefix" | "ImplementationSpecific"; }) { const deployName = `${this.appName}-deploy`; const serviceName = `${this.appName}-svc`; this.namespace = this.cluster.addManifest(`${this.appName}-ns`, { apiVersion: "v1", kind: "Namespace", metadata: { name: args.namespace, }, }); this.deployment = this.cluster.addManifest(deployName, { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: deployName, namespace: args.namespace, labels: { app: this.appName, }, }, spec: { selector: { matchLabels: { app: this.appName, }, }, replicas: args.replicas, template: { metadata: { labels: { app: this.appName, }, }, spec: { containers: [ { name: this.appName, image: args.imageRepo, ports: [ { containerPort: args.targetPort, }, ], }, ], }, }, }, }); this.deployment.node.addDependency(this.namespace); this.service = this.cluster.addManifest(serviceName, { apiVersion: "v1", kind: "Service", metadata: { name: serviceName, namespace: args.namespace, labels: { app: this.appName, }, }, spec: { selector: { app: this.appName, }, ports: [ { protocol: args.protocol, port: args.port, targetPort: args.targetPort, }, ], }, }); this.service.node.addDependency(this.deployment); this.ingress = this.cluster.addManifest(`${this.appName}-ingress`, { apiVersion: "networking.k8s.io/v1", kind: "Ingress", metadata: { name: this.appName, namespace: args.namespace, annotations: { "alb.ingress.kubernetes.io/scheme": args.scheme, "alb.ingress.kubernetes.io/target-type": args.targetType, }, }, spec: { ingressClassName: "alb", rules: [ { http: { paths: [ { path: "/", pathType: args.pathType, backend: { service: { name: serviceName, port: { number: args.port, }, }, }, }, ], }, }, ], }, }); this.ingress.node.addDependency(this.service); } }
Stack
Resource で定義したものを Stack で呼び出し、CDK にてデプロイを行う準備をします。
EKS クラスターのところで先述しましたが、kubectl を実行できる IAM ユーザーを環境変数に定義をし、Stack で呼び出しを行なっています。
他は必要に応じてカスタマイズしてみてください。
# lib/resources/stack/eks-stack.ts import * as cdk from "aws-cdk-lib"; import { Construct } from "constructs"; import { Network } from "../resources/aws/network"; import { EKSCluster } from "../resources/aws/eks"; import { SubnetSelection, SubnetType } from "aws-cdk-lib/aws-ec2"; import { AccessScopeType, AlbControllerVersion, KubernetesVersion, Selector, } from "aws-cdk-lib/aws-eks"; import { KubectlV31Layer } from "@aws-cdk/lambda-layer-kubectl-v31"; import { Manifest } from "../resources/k8s/manifest"; export class HelloEksStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const projectName = "s-tanaka"; const envName = "dev"; //! VPC const vpc_01 = new Network({ scope: this, manageNumber: "01", projectName, envName, }); vpc_01.createVpc({ networkCidr: "10.0.0.0/16", }); //! EKS Cluster const eksVersion = KubernetesVersion.V1_31; const kubectlLayer = new KubectlV31Layer(this, "kubectl"); const eks_01 = new EKSCluster({ scope: this, manageNumber: "01", projectName, envName, }); eks_01.createCluster({ vpc: vpc_01.vpc, version: eksVersion, kubectlLayer, }); //! Fargate Profile const fargate_01_selectors: Selector[] = [ { namespace: "kube-system", }, { namespace: "default", }, { namespace: "app", }, ]; const fargate_01_subnet: SubnetSelection = { subnetType: SubnetType.PRIVATE_WITH_EGRESS, }; eks_01.createFargateProfile({ vpc: vpc_01.vpc, subnet: fargate_01_subnet, selectors: fargate_01_selectors, }); eks_01.createAlbController({ version: AlbControllerVersion.V2_8_2, }); //! EKS Addon eks_01.createAddon({ addonName: "vpc-cni", addonVersion: "v1.18.3-eksbuild.2", }); eks_01.createAddon({ addonName: "coredns", addonVersion: "v1.11.3-eksbuild.1", }); eks_01.createAddon({ addonName: "kube-proxy", addonVersion: "v1.31.0-eksbuild.2", }); //! EKS Access Entry eks_01.createAccessEntry({ iamArn: process.env.MY_IAM_USER!, accessPolicyName: "AmazonEKSClusterAdminPolicy", accessScopeType: AccessScopeType.CLUSTER, manageNumber: "01", }); //! Nginx const nginx = new Manifest({ cluster: eks_01.cluster, appName: "nginx", }); nginx.createService({ namespace: "app", replicas: 3, imageRepo: "public.ecr.aws/docker/library/nginx:latest", protocol: "TCP", port: 80, targetPort: 80, scheme: "internet-facing", targetType: "ip", pathType: "Prefix", }); } }
デプロイ
Stack への記述が完了後、以下のコマンドを実施し構築を行いましょう。
$ cdk deploy
問題なくリソースが作成ができた場合、CFn のコンソールから以下の3つのスタックが作成されていることが確認できます。
構築確認
VPC, EKS クラスター等のリソースの作成が確認できたら kubectl コマンドを用いて、作成されたリソースの詳細を確認します。
kubectl コマンドの導入については以下のドキュメントを参考にしてください。
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/install-kubectl.html
kubeconfig 作成
作成した EKS クラスターへ接続するために認証情報を取得する必要があります。
そのため、以下のコマンドを実行し認証情報の取得を行いましょう。
$ aws eks update-kubeconfig --name { ClusterName } --region $AWS_REGION
各種リソース確認
Node
kubectl get node -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME fargate-ip-10-0-2-163.ap-northeast-1.compute.internal Ready <none> 17m v1.31.1-eks-ce1d5eb 10.0.2.163 <none> Amazon Linux 2 5.10.226-214.879.amzn2.x86_64 containerd://1.7.22 fargate-ip-10-0-2-218.ap-northeast-1.compute.internal Ready <none> 115m v1.31.1-eks-ce1d5eb 10.0.2.218 <none> Amazon Linux 2 5.10.226-214.879.amzn2.x86_64 containerd://1.7.22 fargate-ip-10-0-2-62.ap-northeast-1.compute.internal Ready <none> 115m v1.31.1-eks-ce1d5eb 10.0.2.62 <none> Amazon Linux 2 5.10.226-214.879.amzn2.x86_64 containerd://1.7.22 fargate-ip-10-0-3-162.ap-northeast-1.compute.internal Ready <none> 116m v1.31.1-eks-ce1d5eb 10.0.3.162 <none> Amazon Linux 2 5.10.226-214.879.amzn2.x86_64 containerd://1.7.22 fargate-ip-10-0-3-190.ap-northeast-1.compute.internal Ready <none> 116m v1.31.1-eks-ce1d5eb 10.0.3.190 <none> Amazon Linux 2 5.10.226-214.879.amzn2.x86_64 containerd://1.7.22 fargate-ip-10-0-3-24.ap-northeast-1.compute.internal Ready <none> 17m v1.31.1-eks-ce1d5eb 10.0.3.24 <none> Amazon Linux 2 5.10.226-214.879.amzn2.x86_64 containerd://1.7.22 fargate-ip-10-0-3-83.ap-northeast-1.compute.internal Ready <none> 17m v1.31.1-eks-ce1d5eb 10.0.3.83 <none> Amazon Linux 2 5.10.226-214.879.amzn2.x86_64 containerd://1.7.22 ~/work/hello-eks main !3 ?6 ❯
Deployment
kubectl get deploy -A -o wide NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR app nginx-deploy 3/3 3 3 19m nginx public.ecr.aws/docker/library/nginx:latest app=nginx kube-system aws-load-balancer-controller 2/2 2 2 9h aws-load-balancer-controller 602401143452.dkr.ecr.us-west-2.amazonaws.com/amazon/aws-load-balancer-controller:v2.8.2 app.kubernetes.io/instance=aws-load-balancer-controller,app.kubernetes.io/name=aws-load-balancer-controller kube-system coredns 2/2 2 2 9h coredns 602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/eks/coredns:v1.11.3-eksbuild.1 eks.amazonaws.com/component=coredns,k8s-app=kube-dns ~/work/hello-eks main !3 ?6 ❯
Service
kubectl get svc -A -o wide NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR app nginx-svc ClusterIP 172.20.143.219 <none> 80/TCP 13m app=nginx default kubernetes ClusterIP 172.20.0.1 <none> 443/TCP 9h <none> kube-system aws-load-balancer-webhook-service ClusterIP 172.20.63.42 <none> 443/TCP 9h app.kubernetes.io/instance=aws-load-balancer-controller,app.kubernetes.io/name=aws-load-balancer-controller kube-system kube-dns ClusterIP 172.20.0.10 <none> 53/UDP,53/TCP,9153/TCP 9h k8s-app=kube-dns ~/work/hello-eks main !3 ?6 ❯
Ingress
kubectl get ingress -A -o wide NAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE app nginx alb * k8s-app-nginx-37fa3fa42c-1318496877.ap-northeast-1.elb.amazonaws.com 80 20m ~/work/hello-eks main !3 ?6 ❯
アクセス確認
Ingress に定義されている URL にアクセスすると Nginx の初期ページが出力されていたら問題なく構築が完了できています。
まとめ
いかがでしたでしょうか。
個人的にはマニフェストは TypeScript より Yaml 形式で記述した方が便利だと感じているため、今回のように TypeScript でマニフェストを書く機会は少ないと思いますが、TypeScript でここまでできるのは何かと便利かと思いました。
これを機に 1人でも CDK や EKS を触るきっかけを作れたら幸いです。
次回は竹内さんの「Powershell5系で発生するCLIXML問題を回避してAnsibleを実行する方法」が公開される予定です!お楽しみに!
参考ドキュメント
aws-cdk-lib.aws_ec2 module
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2-readme.html
aws-cdk-lib.aws_eks module
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_eks-readme.html
kube-proxy
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/managing-kube-proxy.html
coredns
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/managing-coredns.html
vpc-cni
https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html
Deployment
https://kubernetes.io/ja/docs/concepts/workloads/controllers/deployment/
ALB Controller
https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/