eksctlで作成していたEKSリソースをterraformに置き換える

## 目的
[EKSのKubernetesバージョンを1.15>1.19へアップデート](https://qiita.com/myu_mx/items/85461d51fa81d8cc23ac)にてeksctlで作成したKubernetesクラスタを作り変えるので、管理上都合の良いterraformに置き換える。

### 課題感
* Infrastructure as Codeの一環としてterraformを使っているが、eksctlで使っている部分がterraform化出来なく統一感がない
    * eksctlで作られるリソースとterraformで作られるリソースの境界線がわかりにくい
    * eksctlの場合リソースを作ることしかできずに変更ができないという制約がある
    * そのためAutoScalingGroupをterraformでも定義する必要があった

## 環境
* EKSのKubernetes1.19を使用する
    * 検証時は1.18でやっていた
* terraform （基本最新版 執筆時v0.14.6）
    * 効率よく作成するため[eks.module](https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/latest)（[v14.0.0](https://github.com/terraform-aws-modules/terraform-aws-eks/releases)）を使用する

## 段取り
1. EKSを動かすのに必要な前提を確認する
    1. 最低限必要なリソース
    2. 弊社の運用上必要なリソース
2. eksctlで作っているリソースを確認する
    1. 明示的に定義しているリソース
    2. 暗示的に定義されるリソース
3. terraformのeks.moduleで作られるリソースを確認する
4. 切替後：eksctlで作成した環境を削除する

執筆上順番建てて書いていますが、実際は1~3を行ったり来たり、動作確認等しながらやりました。

## 前提の確認（抜粋）
* [クラスター VPC に関する考慮事項](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/network_reqs.html)
    * Subnetのタグ付（今見たら1.19から不要になったようです。）
        * Key:kubernetes.io/cluster/\<cluster-name>
        * Valu:shared
* [Amazon EKS クラスターの IAM ロール](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/service_IAM_role.html)
    * [AmazonEKSClusterPolicy](https://console.aws.amazon.com/iam/home#/policies/arn:aws:iam::aws:policy/AmazonEKSClusterPolicy%24jsonEditor)
    * [AmazonEKSServicePolicy](https://console.aws.amazon.com/iam/home#/policies/arn:aws:iam::aws:policy/AmazonEKSServicePolicy%24jsonEditor) >2020 年 4 月 16 日以降に作成されたクラスターで不要になりました。
        * 後述するterraformのeks.moduleで定義されていたので使用している。
* [Amazon EKS ノードIAMロール](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/create-node-role.html)
    * [AmazonEKSWorkerNodePolicy](https://console.aws.amazon.com/iam/home#/policies/arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy%24jsonEditor)
    * [AmazonEC2ContainerRegistryReadOnly](https://console.aws.amazon.com/iam/home#/policies/arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly%24jsonEditor)
* [Amazon EKS セキュリティグループの考慮事項](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/sec-group-reqs.html)

EKSクラスタとワーカーノードが登場するのでごっちゃにならないように注意しましょう。


### 追加で設定している項目
* スポットインスタンスを使用する
    * スポットインスタンスのプールの関係から2種類のインスタンスタイプを指定する
    *  autoScalerにてCapacity,maxSizeを指定する
    *  volumeSize: 50
    *  SSH Keyは事前に用意したものを指定するが、普段はSSHのポートは開けない
* コンテナのログをFluentD経由でCloudWatchLogsに書き込む [参考](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/Container-Insights-setup-logs.html)
    * （Fluent Bit の提供を開始しました。これにより、パフォーマンスの大幅な向上が見込めます） というのに執筆時に気づいた
    * EKSのログはCloudWatchLogsに書き込んでいない
* X-Rayを使う
    * ノードのIAMロールにAWSXRayDaemonWriteAccessのアタッチ
* CodeBuildからデプロイする
    * [aws-auth](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/add-user-role.html)の編集が必要
* （ALBはterraformで作成したものを使用する）
    * [AWS Load Balancer Controller](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/alb-ingress.html)は使用していない
        * EKS側から作るALBで、良し悪しあるみたいですが、terraformで管理したいので使用しません。
        * ECSとかもターゲットにする可能性や、リダイレクト周りのコントロールとかもしているからというのもある。

## eksctlで作られるリソース
eksctlは手っ取り早くEKSでkubernetesクラスタを作成することができる。
VPCからSecurityGroup、IAM周りまで内部的にはCloudFormationで一気に作ることができるので、検証したいときにすごい便利。
[Amazon EKS – eksctlの開始方法](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/getting-started-eksctl.html)など。詳しい使い方は[eksctl](https://eksctl.io/)を参照。

### 明示的に定義するリソース
弊社の場合、大枠としてはVPC,Subnetはterraformで作成し、EKSの部分をeksctlで作成
細かいIDは伏せていますが、実際に使っていたコンフィグファイルを載せます。
（今回精査したら使ってない機能もありましたが、載せています）

```eks-cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: <cluster-name>
  region: ap-northeast-1
  version: "1.15"

vpc:
  id: vpc-0d6f6d357d5817636
  subnets:
    public:
      ap-northeast-1a:
        id: <subnet-ID>
      ap-northeast-1c:
        id: <subnet-ID>
      ap-northeast-1d:
        id: <subnet-ID>

nodeGroups:
  - name: ng-workers-1-15
    desiredCapacity: <num>
    maxSize: <num>
    volumeSize: 50
    volumeType: gp2
    targetGroupARNs:
      - arn:aws:elasticloadbalancing:ap-northeast-1:<accountID>:targetgroup/<targetgroup-name>/<arn>
    instancesDistribution:
      instanceTypes:
        - <instance-1>
        - <instance-2>
      onDemandBaseCapacity: 0 #スポットインスタンスを使うための設定
      onDemandPercentageAboveBaseCapacity: 0
    ssh:
      publicKeyName: <KeyName> #事前に作成しているKeyを指定
      allow: false #eksctl-xxxxx-nodegroup-eks-nodegroup-remoteAccessが作られるらしい
    iam:
      attachPolicyARNs:
        - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy #前提に書いた通り必要
        - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy #terraformのEKS moduleでもデフォルトtrueになっている。使ってなさそうだけどそのままに
        - arn:aws:iam::<accountID>:policy/<policy-name> #諸々アクセスに必要なユーザ管理ポリシー 
      withAddonPolicies:
        imageBuilder: true #ECRへのアクセスが許可されるらしいが、実態としてポリシーがアタッチされていないし、使っていない...
        autoScaler: true #専用のIAMポリシーが作られる
        albIngress: true #ノードのIAMロールにPolicyALBIngressがアタッチされる先述のAWS Load Balancer Controller機能、使ってないので不要
        xRay: true #ノードのIAMロールにAWSXRayDaemonWriteAccessがアタッチされる
        externalDNS: true #ノードのIAMロールにPolicyExternalDNSChangeSetがアタッチされるが、機能を使っていなかった
        cloudWatch: true #ノードのIAMロールにCloudWatchAgentServerPolicyがアタッチされる
    securityGroups:
      withShared: true #すべてのノードグループで共有されるセキュリティグループを使う
      withLocal: true #↓をアタッチする
      attachIDs: ['<sg-ID>'] #ALBからの通信を許可するSG
```

### 自動的に作成されるリソース
[前提の確認（抜粋）](#前提の確認（抜粋）)で書いた内容が作成されている

* Amazon EKS クラスターの IAM ロール
    * AmazonEKSClusterPolicy
    * AmazonEKSServicePolicy 
* Amazon EKS ノードIAMロール
    * AmazonEKSWorkerNodePolicy
    * AmazonEC2ContainerRegistryReadOnly
* EKSクラスタのSG
    * [Amazon EKS セキュリティグループの考慮事項](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/sec-group-reqs.html)に準じていくつか作成される
* ワーカーノードのSG
    * [Amazon EKS セキュリティグループの考慮事項](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/sec-group-reqs.html)に準じていくつか作成される

## terraformのeks.moduleで作られるリソースを確認する
最初はこちら[Provision an EKS Cluster (AWS) | Terraform - HashiCorp Learn](https://learn.hashicorp.com/tutorials/terraform/eks)をベースに環境構築し、動作確認や、差異を比較しながら同等環境になるよう調整した。
大体eks.moduleも[前提の確認（抜粋）](#前提の確認（抜粋）)で書かれている内容がデフォルト（自動的）で作成される。

* Amazon EKS クラスターの IAM ロール
    * AmazonEKSClusterPolicy
    * AmazonEKSServicePolicy（不要らしいがデフォルトであるのでそのままに）
    * AmazonEKS_CNI_Policy（不要らしいがデフォルトであるのでそのままに）
* Amazon EKS ノードIAMロール
    * AmazonEKSWorkerNodePolicy
    * AmazonEC2ContainerRegistryReadOnly
* SG 若干書き方は違うが、考慮事項に準じている
* 細かい部分には差異があり、追加で定義した部分等あるので後述するテンプレートを参照
    * スポットインスタンス周りはこちらを参考にした-[Using spot instances](https://github.com/terraform-aws-modules/terraform-aws-eks/blob/master/docs/spot-instances.md)
    * ボリュームは[gp3の提供が始まった](https://aws.amazon.com/jp/blogs/news/new-amazon-ebs-gp3-volume-lets-you-provision-performance-separate-from-capacity-and-offers-20-lower-price/)ので使っている。gp2に比べてコスト20%ダウン

#### staging/production毎に作成しているリソース

```eks.tf
module "eks" {
  source          = "terraform-aws-modules/eks/aws"
  cluster_name    = "cluster-production-v2"
  cluster_version = "1.19"
  subnets         = [
    var.vpc["subnet_1a"],
    var.vpc["subnet_1c"],
    var.vpc["subnet_1d"]
  ]
  
  tags = { #Environment = "training"は不要なので削除
    "k8s.io/cluster-autoscaler/enabled" = "true"
    "k8s.io/cluster-autoscaler/cluster-production-v2" = "owned"
  }

  vpc_id           = var.vpc["vpc_id"]
  write_kubeconfig = false #ローカルにkubeconfigが置かれるのを無効化

  map_roles = [ #CodeBuildがkubeconfigを取得するためにアタッチしている
    {
      rolearn  = "arn:aws:iam::<accountID>:role/<role-name>"
      username = "codebuild"
      groups   = ["system:masters"]
    }
  ]

  worker_groups_launch_template = [
    {
      name                    = "workers-1-19"
      override_instance_types = ["<instance-1>","<instance-2>"]
      ami_id                  = "<ami-id>" #明示的に指定しないと、amiが最新版ではない場合差異となる
      root_volume_size        = 50
      root_volume_type        = "gp3"
      root_iops               = 3000
      spot_instance_pools     = 2
      asg_min_size            = <num>
      asg_max_size            = <num>
      asg_desired_capacity    = <num>
      enable_monitoring       = false #CloudWatchの詳細メトリクスをオフ
      key_name                = <key-name>
      kubelet_extra_args      = "--node-labels=node.kubernetes.io/lifecycle=spot"
      public_ip               = true
      additional_security_group_ids = [<sg-ID>]
      target_group_arns = [<target_group_arns>]
    },
  ]
}
data "aws_eks_cluster" "cluster" {
  name = module.eks.cluster_id
}

data "aws_eks_cluster_auth" "cluster" {
  name = module.eks.cluster_id
}
```

```kubernetes.tf
provider "kubernetes" {
  host                   = data.aws_eks_cluster.cluster.endpoint
  token                  = data.aws_eks_cluster_auth.cluster.token
  cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data)
}
```

```iam.tf
resource "aws_iam_role_policy_attachment" "worker_group" {
  role       = module.eks.worker_iam_role_name
  policy_arn = var.eks["cluster-auto-scaler"] #cluster-auto-scaler.jsonで作られるIAMポリシー
}

resource "aws_iam_role_policy_attachment" "node" {
  role       = module.eks.worker_iam_role_name
  policy_arn = <policy-arn> #ユーザ管理のポリシー
}

resource "aws_iam_role_policy_attachment" "xray" {
  role       = module.eks.worker_iam_role_name
  policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
}

resource "aws_iam_role_policy_attachment" "cloud_watch" {
  role       = module.eks.worker_iam_role_name
  policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
}
```

```variables.tf 
variable "eks" {
  type = map(string)
  default = {
    cluster-auto-scaler   = "<arn>"
  }
}
```


#### 共通で定義しているリソース
[Cluster Autoscaler-IAM ポリシーとロールを作成する](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/cluster-autoscaler.html#ca-create-policy)を元にポリシーを作成
（執筆時に見つけた）[autoscaling.md](https://github.com/terraform-aws-modules/terraform-aws-eks/blob/master/docs/autoscaling.md)を見るとhelmで定義できるけど、自動的に作られてしまうので、今回の方法で良いかも。

```iam.tf
resource "aws_iam_policy" "cluster_auto_scaler" {
  name   = "cluster-auto-scaler"
  path   = "/"
  policy = file("iam/cluster-auto-scaler.json")
}
```

```cluster-auto-scaler.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "autoscaling:DescribeAutoScalingGroups",
                "autoscaling:DescribeAutoScalingInstances",
                "autoscaling:DescribeLaunchConfigurations",
                "autoscaling:DescribeTags",
                "autoscaling:SetDesiredCapacity",
                "autoscaling:TerminateInstanceInAutoScalingGroup",
                "ec2:DescribeLaunchTemplateVersions"
            ],
            "Resource": "*"
        }
    ]
}
```

subnetのタグは1.18の情報をベースに準備していたので付与している。1.19を使う場合はおそらく不要なはず

```vpc.tf
resource "aws_subnet" "subnet-pf-public-1" {
  vpc_id                  = aws_vpc.vpc-pf.id
  cidr_block              = var.vpc-pf.subnet_pub1_cidr_block
  availability_zone       = var.vpc-pf.subnet_pub1_az
  map_public_ip_on_launch = true
  tags = {
    "kubernetes.io/cluster/cluster-staging-v2"    = "shared"
    "kubernetes.io/cluster/cluster-production-v2" = "shared"
  }
```

## 切替後：eksctlで作成した環境を削除する
1. asg.tfの削除(staging/production)
    * AutoScalingGroupはterraformで差分が出てしまい、明示的に定義していたもの
    * ファイルを削除し `terraform apply`
2. eksctlでclusterの削除(staging/production)
    * `eksctl delete cluster -f <コンフィグファイル名>`で一発
    * CloudFormationのStackやEKS等々を見て削除を確認



## おわりに
差分を比較すると細かい部分に目が行き、時間がかかったりする部分がありますが、AWSのドキュメントに載っている内容が満たされているかつサービス基盤として各動作が問題なければ割り切って置き換えてしまって良いかと思います。
これで晴れてterraformで統一的にリソース管理ができるようになりました。


[Global Mobility Serviceでは、インフラエンジニア含めITエンジニアを募集しています。](https://recruit.global-mobility-service.com/)