AWS CDKでEKSクラスタを作ってみた

AWS CDKが今日も楽しいです。奥です。
るんるん気分で触ったことがなかったEKSクラスタを作成しました。るん。

本記事ではAWS CDKを使って薄くEKSクラスタをどのように作ったかを記載します。 TypeScriptやAWS CDKの基本的な部分は記載しません。 また、本ブログで使用した、AWS CDKのバージョンは1.4.0です。

目次

プロジェクトの初期設定

AWS CDKのスタックを作成するための初期設定を行ないます。 個人の趣向ですが、cdk initをせずにプロジェクトの設定を行います。

$ mkdir cdk-eks-101 && cd $_
$ mkdir src
$ git init
$ npm init -y
$ npm i @aws-cdk/{core,aws-eks,aws-ec2,aws-iam}
$ npm i -D aws-cdk @types/node typescript

次に、package,jsonの設定をしてます。
下記のscriptsの部分を変更してください。

{
  "name": "cdk-eks-101",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "deploy": "tsc && cdk deploy"
  },
  "author": "37108",
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^12.7.2",
    "aws-cdk": "^1.4.0",
    "typescript": "^3.5.3"
  },
  "dependencies": {
    "@aws-cdk/aws-ec2": "^1.4.0",
    "@aws-cdk/aws-eks": "^1.4.0",
    "@aws-cdk/aws-iam": "^1.4.0",
    "@aws-cdk/core": "^1.4.0"
  }
}

ハイライトした部分の変更はできましたか。authorLICENSEなどの細かい部分は人によってまちまちなのであまり気にしないでください。

次に、tsconfig.jsonを作成します。
あまりルールを厳しくはしていないのでお好みで変えてください。

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "outDir": "./dist/",
    "lib": [ "es2016", "es2017.object", "es2017.string" ],
    "noUnusedLocals": false,
    "noUnusedParameters": false
  }
}

最後にcdk.jsonを追加して初期設定は終わりです。

{
  "app": "node dist/index"
}

これでプロジェクトの初期設定は終了です。

CDKのコーディング

コードを書いて、EKSクラスタを作成します。

クラスの作成

まずはクラスを作成します。
いつも通り、cdk.Stackクラスを継承して新規クラスを作成します。 作成したクラスを元にインスタンスを作成する際に、regionを渡してください。 現在のバージョンでは、EKSクラスタを作成する場合は指定しないとCDKスタックの作成に失敗します。
実際にここを指定しないで、CDKスタックを作成しようとすると、Unable to determine AMI from AMI map since stack is region-agnosticとエラーがでます。

#!/usr/bin/env node
import cdk = require('@aws-cdk/core')

export class EKS101Stack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)
  }
}

const app = new cdk.App()
new EKS101Stack(app, 'EKS101Stack', {
  env: {
    region: 'ap-northeast-1'
  }
})
app.synth()

VPCの作成

EKSクラスタのノードが稼働するためのVPCと、サブネットを作成します。

#!/usr/bin/env node
import cdk = require('@aws-cdk/core')
import { Vpc, InstanceType, SubnetType } from '@aws-cdk/aws-ec2'

export class EKS101Stack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const vpc = new Vpc(this, 'vpc', {
      cidr: '192.168.0.0/16',
      natGateways: 1,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Public1',
          subnetType: SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'Public2',
          subnetType: SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'Private1',
          subnetType: SubnetType.PRIVATE,
        },
        {
          cidrMask: 24,
          name: 'Private2',
          subnetType: SubnetType.PRIVATE,
        },
      ]
    })
  }
}

const app = new cdk.App()
new EKS101Stack(app, 'EKS101Stack', {
  env: {
    region: 'ap-northeast-1'
  }
})
app.synth()

IAM ロールの作成

EKSクラスタのマスタノード用にアタッチするIAM ロールを作成します。
実際に本番環境で作成する場合は権限などを十分に検討して作成してください。

#!/usr/bin/env node
import cdk = require('@aws-cdk/core')
import { Vpc, InstanceType, SubnetType } from '@aws-cdk/aws-ec2'
import { Role, ServicePrincipal, ManagedPolicy } from '@aws-cdk/aws-iam'

export class EKS101Stack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const vpc = new Vpc(this, 'vpc', {
      cidr: '192.168.0.0/16',
      natGateways: 1,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Public1',
          subnetType: SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'Public2',
          subnetType: SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'Private1',
          subnetType: SubnetType.PRIVATE,
        },
        {
          cidrMask: 24,
          name: 'Private2',
          subnetType: SubnetType.PRIVATE,
        },
      ]
    })

    const eksRole = new Role(this, 'eksRole', {
      assumedBy: new ServicePrincipal('eks.amazonaws.com')
    })
    eksRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSClusterPolicy'))
    eksRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSServicePolicy'))
  }
}

const app = new cdk.App()
new EKS101Stack(app, 'EKS101Stack', {
  env: {
    region: 'ap-northeast-1'
  }
})
app.synth()

Deployment、Serviceの定義

EKSクラスタで使用する、Deployment、Serviceを定義します。
可読性を考慮して、別ファイルに出します。

const appLabel = { app: "hello-kubernetes" }

export const deployment = {
  apiVersion: "apps/v1",
  kind: "Deployment",
  metadata: { name: "hello-kubernetes" },
  spec: {
    replicas: 3,
    selector: { matchLabels: appLabel },
    template: {
      metadata: { labels: appLabel },
      spec: {
        containers: [
          {
            name: "hello-kubernetes",
            image: "paulbouwer/hello-kubernetes:1.5",
            ports: [ { containerPort: 8080 } ]
          }
        ]
      }
    }
  }
}

export const service = {
  apiVersion: "v1",
  kind: "Service",
  metadata: { name: "hello-kubernetes" },
  spec: {
    type: "LoadBalancer",
    ports: [ { port: 80, targetPort: 8080 } ],
    selector: appLabel
  }
}

クラスタの作成

EKSクラスタを作成して、先ほど作成したDeployment、Serviceを追加していきます。
clusterに対して、addCapacityでEC2インスタンスを追加していますが、デフォルトで2台EC2インスタンスが作成されるので合計4台のインスタンスが起動されます。 多いという場合は、addCapacityを使わないか、clusterの定義時に、デフォルトの台数を0にするとよいでしょう。
また、Auto Scalingも使用可能なので、ユースケースに合わせて対応することが可能です。  

#!/usr/bin/env node
import cdk = require('@aws-cdk/core')
import { Vpc, InstanceType, SubnetType } from '@aws-cdk/aws-ec2'
import { Role, ServicePrincipal, ManagedPolicy } from '@aws-cdk/aws-iam'
import { Cluster } from '@aws-cdk/aws-eks'

import { deployment, service } from './manifest'

export class EKS101Stack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const vpc = new Vpc(this, 'vpc', {
      cidr: '192.168.0.0/16',
      natGateways: 1,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Public1',
          subnetType: SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'Public2',
          subnetType: SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'Private1',
          subnetType: SubnetType.PRIVATE,
        },
        {
          cidrMask: 24,
          name: 'Private2',
          subnetType: SubnetType.PRIVATE,
        },
      ]
    })

    const eksRole = new Role(this, 'eksRole', {
      assumedBy: new ServicePrincipal('eks.amazonaws.com')
    })
    eksRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSClusterPolicy'))
    eksRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSServicePolicy'))

    const cluster = new Cluster(this, 'cluster', {
      vpc,
      mastersRole: eksRole,
      clusterName: 'boringWozniak',
    })
    cluster.addCapacity('capacity', {
      desiredCapacity: 2,
      instanceType: new InstanceType('m5.large'),
    })
    cluster.addResource('resource', deployment, service)
  }
}

const app = new cdk.App()
new EKS101Stack(app, 'EKS101Stack', {
  env: {
    region: 'ap-northeast-1'
  }
})
app.synth()

コーディングを振り返ってみると少ない記述でEKSクラスタが定義できていますね。

デプロイ

次はCDKスタックをデプロイしたいと思います。
CDK側の都合でS3バケットを作成する必要があるため、いきなりcdk deployをするとエラーがでます。
S3オブジェクトを確認したところ、EKS用に使用するためのPythonスクリプトのファイルが入ってました。

Bootstrapping

cdk deployの前に、Bootstrappingを行います。
これを行うと新たに、CDKToolkitというCFnスタックが作成され必要なファイルがS3に格納されます。
cdk destroyではこのCFnスタックは削除されないので注意してください。

$ npx cdk bootstrap aws:///ap-northeast-1
⏳ Bootstrapping environment aws:///ap-northeast-1...
CDKToolkit: creating CloudFormation changeset...
✅ Environment aws:///ap-northeast-1 bootstrapped.

Deploy

準備が終わったので、cdk deployを行います。
かなり時間がかかりますが、完了後にCFnスタックが確認できます。

$ npm run deploy
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)

Do you wish to deploy these changes (y/n)? y
EKS101Stack: deploying...
EKS101Stack: creating CloudFormation changeset...
✅ EKS101Stack

無事に出来上がりました。

確認

作成したリソースを確認してみます。
AWSマネジメントコンソールからEKSにアクセスしてみます。
無事にクラスタが出来上がってますね。

さいごに

クラスノードに対して必要なIAM ロールをよしなに作ってくれたりSecurity Groupが必要分作成されたりと、少ない記述量でEKSクラスタができるという事実に驚愕しています。
今回は薄く実装しただけなので、もっと実用的に記載する機会があればと思ってます。