Pulumi で Amazon EKS クラスター + kubernetes の deployment と service を構築

はじめに

前回の記事 では Pulumi の簡単なセットアップと使用方法などをまとめてみました。
今回は AWS 上に EKS のクラスタと kubernetes の deployment と service を構築する際に Pulumi を使用してみました。

今回のサンプルコードは GitHub にあるので良ければ参考にしてみてください。

概要

今回は以下のような構成で作成していきます。

aws-pulumi-eks

EKS クラスターの仮想ネットワーク

EKS クラスターの前提条件として、2 つ以上の異なる AZ のパブリックサブネットが必要です。
そのため、東京リージョン(ap-northeast-1)上にap-northeast-1a, ap-northeast-1cの AZ 上にサブネットをそれぞれ 1 つずつ作成しました。
今回の構成ではデータベースがないため、プライベートサブネットは用意していません。

EKS クラスターの権限周り

EKS クラスターと EKS ワーカーノード用 それぞれに、 eksClusterRole, eksWorkerRole という 2 つの IAM ロールを用意します。
また、それぞれに以下の IAM Policy を設定します。

参考 - AWS 公式ドキュメント AmazonEKSClusterPolicy

クラスターを作成する前に、このポリシーをアタッチしたクラスター IAM ロールが必要となります。

参考 - AWS 公式ドキュメント AmazonEKS_CNI_Policy

AmazonEKS_CNI_Policy ポリシーを IAM エンティティにアタッチできます。Amazon EC2 ノードグループを作成する前に、このポリシーをノード IAM ロール、または AWS VPC CNI プラグインで使用する IAM ロールにアタッチする必要があります

参考 - AWS 公式ドキュメント AmazonEKSWorkerNodePolicy

このポリシーを、Amazon EKS がユーザーに代わってアクションを実行することを許可する Amazon EC2 ノードを作成するときに指定するノード IAM ロールにアタッチしなければなりません。

  • eksClusterRole
    • AmazonEKSClusterPolicy Kubernetes クラスターが AWS のサービスを呼び出す際に必要
  • eksWorkerRole
    • AmazonEKSWorkerNodePolicy インスタンスボリュームとネットワーク情報の読み取り、ワーカーノードからの EKS クラスターアクセス用
    • AmazonEKS_CNI_Policy ネットワーク設定変更用
    • AmazonEC2ContainerRegistryReadOnly ECR のイメージを参照するために必要

コードに関して説明

実際のコードになります。

GitHub - warawara28/pulumi-sample-aws-eks-cluster: AWS EKS Cluster sample with Pulumi

サンプルコードでは awskubernetes という 2 つのプロジェクトに分けていて以下のように分離しています。

  • aws AWS のリソース (VPC やサブネットなどのネットワーク、EKS クラスターととワーカーノードが含まれる)
  • kubernetes k8s のマニフェスト

AWS のリソース

ネットワーク周りの作成

コード

ネットワーク関連で以下のリソースを作成しています。

  • Vpc 1 つ
  • Subnet 2 つ
  • インターネット接続用の InternetGateway 1 つ
  • それぞれのサブネットに対応する RouteRableRouteTableAssociation それぞれ 2 つずつ
const vpcName: string = `${projectName}-vpc`;
const vpc = new aws.ec2.Vpc(vpcName, {
  cidrBlock: '10.0.0.0/16',
  tags: {
    Name: vpcName,
  },
});

const internetGatewayName: string = `${projectName}-internet-gateway`;
const internetGateway = new aws.ec2.InternetGateway(internetGatewayName, {
  vpcId: vpc.id,
  tags: {
    Name: internetGatewayName,
  },
});

const subnetNameA = `${projectName}-subnet-a`;
const subnetA = new aws.ec2.Subnet(subnetNameA, {
  vpcId: vpc.id,
  availabilityZone: 'ap-northeast-1a',
  cidrBlock: '10.0.1.0/24',
  mapPublicIpOnLaunch: true,
  tags: {
    Name: subnetNameA,
  },
});

const subnetNameC = `${projectName}-subnet-c`;
const subnetC = new aws.ec2.Subnet(subnetNameC, {
  vpcId: vpc.id,
  availabilityZone: 'ap-northeast-1c',
  cidrBlock: '10.0.2.0/24',
  mapPublicIpOnLaunch: true,
  tags: {
    Name: subnetNameC,
  },
});

const routetableNameA: string = `${subnetNameA}-routetable`;
const routetableA = new aws.ec2.RouteTable(routetableNameA, {
  vpcId: vpc.id,
  routes: [
    {
      cidrBlock: '0.0.0.0/0',
      gatewayId: internetGateway.id,
    },
  ],
  tags: {
    Name: routetableNameA,
  },
});

const routetableNameC: string = `${subnetNameC}-routetable`;
const routetableC = new aws.ec2.RouteTable(routetableNameC, {
  vpcId: vpc.id,
  routes: [
    {
      cidrBlock: '0.0.0.0/0',
      gatewayId: internetGateway.id,
    },
  ],
  tags: {
    Name: routetableNameC,
  },
});
const routeTableAssociationA = new aws.ec2.RouteTableAssociation(
  `${subnetNameA}-rta`,
  {
    subnetId: subnetA.id,
    routeTableId: routetableA.id,
  }
);
const routeTableAssociationC = new aws.ec2.RouteTableAssociation(
  `${subnetNameC}-rta`,
  {
    subnetId: subnetC.id,
    routeTableId: routetableC.id,
  }
);

EKS クラスタとノードグループの作成

コード

EKS クラスタで以下のリソースを作成しています。

  • Role 2 つ
    • eksClusterRole
    • eksWorkerRole
  • Cluster 1 つ
  • NodeGroup 1 つ

今回は t3.micro のインスタンスを 2 つ用意した NodeGroup を用意します。

const eksClusterRole = new aws.iam.Role(`${projectName}-eks-cluster-role`, {
  assumeRolePolicy: JSON.stringify({
    Version: '2012-10-17',
    Statement: [
      {
        Action: 'sts:AssumeRole',
        Effect: 'Allow',
        Sid: '',
        Principal: {
          Service: 'eks.amazonaws.com',
        },
      },
    ],
  }),
});
const eksClusterRoleAttach1 = new aws.iam.RolePolicyAttachment(
  `role-policy-attachment-eks-cluster-policy`,
  {
    role: eksClusterRole.name,
    policyArn: 'arn:aws:iam::aws:policy/AmazonEKSClusterPolicy',
  }
);

const eksWorkerRole = new aws.iam.Role(`${projectName}-eks-worker-role`, {
  assumeRolePolicy: JSON.stringify({
    Version: '2012-10-17',
    Statement: [
      {
        Action: 'sts:AssumeRole',
        Effect: 'Allow',
        Sid: '',
        Principal: {
          Service: 'ec2.amazonaws.com',
        },
      },
    ],
  }),
});

const eksWorkerRoleAttach1 = new aws.iam.RolePolicyAttachment(
  `role-policy-attachment-eks-worker-node-policy`,
  {
    role: eksWorkerRole.name,
    policyArn: 'arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy',
  }
);
const eksWorkerRoleAttach2 = new aws.iam.RolePolicyAttachment(
  `role-policy-attachment-eks-cni-policy`,
  {
    role: eksWorkerRole.name,
    policyArn: 'arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy',
  }
);
const eksWorkerRoleAttach3 = new aws.iam.RolePolicyAttachment(
  `role-policy-attachment-ecr-readonly-policy`,
  {
    role: eksWorkerRole.name,
    policyArn: 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly',
  }
);

const eksCluster = new aws.eks.Cluster(
  `${projectName}-eks-cluster`,
  {
    roleArn: eksClusterRole.arn,
    vpcConfig: {
      subnetIds: [subnetA.id, subnetC.id],
    },
  },
  {}
);

const nodeGroupName: string = `${projectName}-eks-nodegroup`;
const nodeGroup = new aws.eks.NodeGroup(
  nodeGroupName,
  {
    clusterName: eksCluster.name,
    nodeRoleArn: eksWorkerRole.arn,
    subnetIds: [subnetA.id, subnetC.id],
    scalingConfig: {
      desiredSize: 2,
      maxSize: 2,
      minSize: 2,
    },
    nodeGroupName: nodeGroupName,
    diskSize: 10,
    instanceTypes: ['t3.micro'],
    updateConfig: {
      maxUnavailable: 1,
    },
  },
  {}
);

k8s のリソース作成

コード

kubernetes のリソースとして以下を作成します。

  • deployment
  • service

実際の運用では チームやサービスごとに namespace を作成する場合もあると思いますが、今回は最小構成で作成します。
今回はイメージとして公式ドキュメントのサンプルアプリケーションをデプロイするでも使用されている public.ecr.aws/nginx/nginx:1.21 のイメージを使用します。

const deploymentName = `${serviceName}-deployment`;
const appLabels = { app: deploymentName };
const deployment = new k8s.apps.v1.Deployment(deploymentName, {
  metadata: {
    name: deploymentName,
    labels: appLabels,
  },
  spec: {
    replicas: 2,
    selector: { matchLabels: appLabels },
    template: {
      metadata: { labels: appLabels },
      spec: {
        containers: [
          {
            name: deploymentName,
            image: 'public.ecr.aws/nginx/nginx:1.21',
            ports: [{ name: 'http', containerPort: 80 }],
            imagePullPolicy: 'IfNotPresent',
          },
        ],
      },
    },
  },
});

const k8sServiceName = `${serviceName}-service`;
const service = new k8s.core.v1.Service(
  k8sServiceName,
  {
    metadata: {
      name: k8sServiceName,
      labels: appLabels,
    },
    spec: {
      type: 'LoadBalancer',
      selector: appLabels,
      ports: [{ protocol: 'TCP', port: 80, targetPort: 'http' }],
    },
  },
  {
    dependsOn: deployment,
  }
);

実際に作成する

サンプルコードを元に aws の方で記載されている AWS のリソースをデプロイします。
デプロイ前に npm install でモジュールをインストールします。

> npm install
npm WARN deprecated [email protected]: The functionality that this package provided is now in @npmcli/arborist
npm WARN deprecated [email protected]: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
npm WARN deprecated [email protected]: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.

added 124 packages, and audited 125 packages in 7s

32 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

その後 pulumi up でデプロイします。
EKS のクラスタの作成にはおおよそ 10 分程かかります。

> pulumi up
Previewing update (dev)

View Live: https://~~~

     Type                              Name                                           Plan
 +   pulumi:pulumi:Stack               pulumi-sample-aws-dev                          create
 +   ├─ aws:iam:Role                   pulumi-sample-aws-eks-cluster-role             create
 +   ├─ aws:ec2:Vpc                    pulumi-sample-aws-vpc                          create
 +   ├─ aws:iam:Role                   pulumi-sample-aws-eks-worker-role              create
 +   ├─ aws:iam:RolePolicyAttachment   role-policy-attachment-eks-cluster-policy      create
 +   ├─ aws:iam:RolePolicyAttachment   role-policy-attachment-ecr-readonly-policy     create
 +   ├─ aws:iam:RolePolicyAttachment   role-policy-attachment-eks-cni-policy          create
 +   ├─ aws:iam:RolePolicyAttachment   role-policy-attachment-eks-worker-node-policy  create
 +   ├─ aws:ec2:InternetGateway        pulumi-sample-aws-internet-gateway             create
 +   ├─ aws:ec2:Subnet                 pulumi-sample-aws-subnet-a                     create
 +   ├─ aws:ec2:Subnet                 pulumi-sample-aws-subnet-c                     create
 +   ├─ aws:eks:Cluster                pulumi-sample-aws-eks-cluster                  create
 +   ├─ aws:ec2:RouteTable             pulumi-sample-aws-subnet-c-routetable          create
 +   ├─ aws:ec2:RouteTable             pulumi-sample-aws-subnet-a-routetable          create
 +   ├─ aws:ec2:RouteTableAssociation  pulumi-sample-aws-subnet-c-rta                 create
 +   ├─ aws:ec2:RouteTableAssociation  pulumi-sample-aws-subnet-a-rta                 create
 +   └─ aws:eks:NodeGroup              pulumi-sample-aws-eks-nodegroup                create

Outputs:
  + eksClusterName: "pulumi-sample-aws-eks-cluster-ca4d43d"

Resources:
    + 17 to create

Do you want to perform this update? yes
Updating (dev)

View Live: https://~~~

     Type                              Name                                           Status
 +   pulumi:pulumi:Stack               pulumi-sample-aws-dev                          created
 +   ├─ aws:iam:Role                   pulumi-sample-aws-eks-cluster-role             created
 +   ├─ aws:ec2:Vpc                    pulumi-sample-aws-vpc                          created
 +   ├─ aws:iam:Role                   pulumi-sample-aws-eks-worker-role              created
 +   ├─ aws:iam:RolePolicyAttachment   role-policy-attachment-eks-cluster-policy      created
 +   ├─ aws:iam:RolePolicyAttachment   role-policy-attachment-eks-cni-policy          created
 +   ├─ aws:iam:RolePolicyAttachment   role-policy-attachment-ecr-readonly-policy     created
 +   ├─ aws:iam:RolePolicyAttachment   role-policy-attachment-eks-worker-node-policy  created
 +   ├─ aws:ec2:InternetGateway        pulumi-sample-aws-internet-gateway             created
 +   ├─ aws:ec2:Subnet                 pulumi-sample-aws-subnet-a                     created
 +   ├─ aws:ec2:Subnet                 pulumi-sample-aws-subnet-c                     created
 +   ├─ aws:ec2:RouteTable             pulumi-sample-aws-subnet-c-routetable          created
 +   ├─ aws:ec2:RouteTable             pulumi-sample-aws-subnet-a-routetable          created
 +   ├─ aws:ec2:RouteTableAssociation  pulumi-sample-aws-subnet-c-rta                 created
 +   ├─ aws:ec2:RouteTableAssociation  pulumi-sample-aws-subnet-a-rta                 created
 +   ├─ aws:eks:Cluster                pulumi-sample-aws-eks-cluster                  created
 +   └─ aws:eks:NodeGroup              pulumi-sample-aws-eks-nodegroup                created

Outputs:
  + eksClusterName: "pulumi-sample-aws-eks-cluster-ca4d43d"

Resources:
    + 17 created

Duration: 12m46s

AWS のリソースをデプロイ後、Output に出力された クラスター名を確認しておきます。
今回は pulumi-sample-aws-eks-cluster-ca4d43d になります。(リソース名に接尾辞としてランダムな英数字が付与されます。)

EKS のクラスター、ノードグループが作成されましたので、kubernetes の service , deployment を同様にデプロイします。
デプロイ前に README に書かれたように npm installkubeconfig の作成を行います。

> npm install
npm WARN deprecated [email protected]: The functionality that this package provided is now in @npmcli/arborist
npm WARN deprecated [email protected]: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
npm WARN deprecated [email protected]: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.

added 142 packages, and audited 143 packages in 20s

32 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

> aws eks update-kubeconfig --region ap-northeast-1 --profile pulumi-sample --name pulumi-sample-aws-eks-cluster-ca4d43d
Added new context arn:aws:eks:ap-northeast-1:xxxxxxxxxx:cluster/pulumi-sample-aws-eks-cluster-ca4d43d to ~~~

準備が完了したら、同様にpulumi up でデプロイします。

> pulumi up
Previewing update (dev)

View Live: https://~~~

     Type                              Name                              Plan
 +   pulumi:pulumi:Stack               pulumi-sample-service-dev         create
 +   ├─ kubernetes:apps/v1:Deployment  pulumi-sample-service-deployment  create
 +   └─ kubernetes:core/v1:Service     pulumi-sample-service-service     create

Outputs:
  + loadBalancerHost: "ae9ad1a877e6d4fe5a952627ba6d7768-1194268529.ap-northeast-1.elb.amazonaws.com"

Resources:
    + 3 to create

Do you want to perform this update? yes
Updating (dev)

View Live: https://~~~

     Type                              Name                              Status
 +   pulumi:pulumi:Stack               pulumi-sample-service-dev         created
 +   ├─ kubernetes:apps/v1:Deployment  pulumi-sample-service-deployment  created
 +   └─ kubernetes:core/v1:Service     pulumi-sample-service-service     created

Outputs:
  + loadBalancerHost: "ae9ad1a877e6d4fe5a952627ba6d7768-1194268529.ap-northeast-1.elb.amazonaws.com"

Resources:
    + 3 created

Duration: 25s

kubernetes のデプロイ完了後に Output に出力されたホストにアクセスします。

https://ae9ad1a877e6d4fe5a952627ba6d7768-1194268529.ap-northeast-1.elb.amazonaws.com/

成功すれば以下のように nginx から返された HTML が表示されます。

aws-pulumi-eks

作成後は料金がかからないように pulumi destroy で削除します。
(以下の例の場合はスタックも削除しています。)
kubernetes のリソースを削除した後に、 aws のリソースを削除します。

> pulumi destroy
Previewing destroy (dev)

View Live: https://~~~

     Type                              Name                              Plan
 -   pulumi:pulumi:Stack               pulumi-sample-service-dev         delete
 -   ├─ kubernetes:core/v1:Service     pulumi-sample-service-service     delete
 -   └─ kubernetes:apps/v1:Deployment  pulumi-sample-service-deployment  delete

Outputs:
  - loadBalancerHost: "ae9ad1a877e6d4fe5a952627ba6d7768-1194268529.ap-northeast-1.elb.amazonaws.com"

Resources:
    - 3 to delete

Do you want to perform this destroy? yes
Destroying (dev)

View Live: https://~~~

     Type                              Name                              Status
 -   pulumi:pulumi:Stack               pulumi-sample-service-dev         deleted
 -   ├─ kubernetes:core/v1:Service     pulumi-sample-service-service     deleted
 -   └─ kubernetes:apps/v1:Deployment  pulumi-sample-service-deployment  deleted

Outputs:
  - loadBalancerHost: "ae9ad1a877e6d4fe5a952627ba6d7768-1194268529.ap-northeast-1.elb.amazonaws.com"

Resources:
    - 3 deleted

Duration: 25s

The resources in the stack have been deleted, but the history and configuration associated with the stack are still maintained.
If you want to remove the stack completely, run 'pulumi stack rm dev'.

> pulumi stack rm dev
This will permanently remove the 'dev' stack!
Please confirm that this is what you'd like to do by typing ("dev"): dev
Stack 'dev' has been removed

おわりに

今回は EKS 上でサンプルアプリケーションとして nginx のコンテナイメージをデプロイしてみました。
実際のサービスとして運用する際にはログ周りや監視、DB との接続 それらに加えて k8s のマニフェストの設定の修正 などが必要になってきます。
既存のサービスを IaC 化する際の第一歩としてこの記事が参考になれば幸いです。