TerraformとAmazon EKSを使ったお手軽k8s環境の作り方

# お約束
- 本記事は [ARISE analytics Advent Calendar 2022](https://qiita.com/advent-calendar/2022/arise-analytics) の16日目の記事です
- 昨日は @sakaihiroaki さんの [Flutterでアクションゲーム開発（Flappy Bird）をやってみた](https://qiita.com/sakaihiroaki/items/83d8d0143c9d3d620a3d)

# はじめに
以前Kubernetesについて学ぶ機会があったので、今回はクラウドでお手軽にk8s環境を構築する方法をご紹介します！
これから書く内容は[こちらの素晴らしい記事](https://developer.mamezou-tech.com/containers/k8s/tutorial/ingress/ingress-aws/)の内容を大いに参考にさせていただいているので、説明不十分な箇所があればこちらを覗いてみると解決するかもです。

# Kubernetesについて
Kubernetes(以下、k8sと記述）については、インターネットの海に大量に情報が落ちているので、ここでは軽く触れるだけにします。公式では以下のように紹介されています。

>Kubernetesは、宣言的な構成管理と自動化を促進し、コンテナ化されたワークロードやサービスを管理するための、ポータブルで拡張性のあるオープンソースのプラットフォームです。Kubernetesは巨大で急速に成長しているエコシステムを備えており、それらのサービス、サポート、ツールは幅広い形で利用可能です。

https://kubernetes.io/ja/docs/concepts/overview/what-is-kubernetes/

宣言的であるとは、あるべき状態を定義するとそれに合わせて現在の状態を変化させることであるべき状態が実現される仕組みがあることを言います。
k8sでは、様々な役割を持つ各種コンポーネントによってクラスターが構成されますが、それらはいずれもyaml形式のファイル（マニフェストと呼ばれる）で宣言的に定義されます。クラスターの司令的な役割を担うノードがこのマニフェストを読み取り、最終的にマニフェストに定義された状態を実現しようとします。
「宣言的である」と並列でよく語られるのが「手続き的である」ですが、どちらが良い悪いといった話ではなくそれぞれにメリットデメリットがあり、k8sは宣言的であることのメリットを享受した仕組みであると言えます。この辺の違いは[こちらのサイト](https://zenn.dev/masahiro_toba/books/a808e953a39110/viewer/0383a4)で挙げられている例がわかりやすいかと思います。

k8sはこのようにマニフェストを用いてクラスター構成を管理し、宣言的にクラスターのあるべき状態を定義することで、その状態を実現するために必要な様々な手続きを意識することなく自動で実行してくれるというのが利点になっています。

ワークロードやサービスといっているのは、おそらくバッチ的な処理やWebアプリケーションのことを指していると思いますが、k8s上のワークロードやサービスは全てコンテナ上で動作しており、これによってワークロード・サービスのスケールや監視等が容易になっています。


# Terraformについて
Terraformは最近流行りのIaCを実現するためのツールです。
↓に書いてある通り、コンピュータやネットワークの構築を自動化することができて、AWSやAzure、GCPなど複数のクラウドで使えます。

> HashiCorp社が提供するTerraformは、マルチクラウド上のコンピュータやネットワークの構築を自動化する、エンジニアにとても人気のあるツールです。

https://www.lac.co.jp/lacwatch/service/20200903_002270.html

AWSなどのクラウドサービスを使ったことがある方はわかると思いますが、GUIのコンソール上でぽちぽちマウスカーソルをクリックしてVPCやEC2なんかを作成してしまうと、しばらくたってから全く同じ構成で新しいシステムを構築しないといけなくなった場合だとか、インフラ担当者の交代時に引き継ぎ漏れが生じた場合なんかに、一体今このシステムはどんな構成で動いているのかが一見してわからなくて困ってしまうんですよね。コンソール画面を見てある程度どんな構成かはわかるかもしれませんが、細かなところまで把握するには正確な仕様書などがないと厳しいかと思います。

Terraformでは、システム構築時にコンソール上で実施する手続きをコードとして記述することで、再現性のあるシステムを構築することができるようになります。システムを新たに引き継ぐ人であったり、最初に構築した際の記憶を忘れてしまった自分であっても、コードを見ることで詳細まで構成を把握することができます。また、途中でシステムの構成を変えられるので、コードをバージョン管理してあげることでシステム構成の変遷を辿ることなんかもできます。


# TerraformによるEKSクラスター構築
今回は、Amazon EKSを使ってk8s環境を構築していきます。
後に出てくるterraformのコードやマニフェストファイルなどをコピペしていけば構築できるように意識しているので、気軽に見ていただければと思います。

## 出来上がりイメージ
パパッと作ったのでもしかしたら間違い等あるかもですが、お手軽ということでこんな感じのシステム構成で作っていきます！
アプリはフロントとバックエンドを分けてDB作って…ってな感じでもう少し凝ったものを展開したいところですが、今回は簡潔にnginxのコンテナだけを乗っけてアクセスできるようにすることを目標にします。冗長性を考慮してnginxを二つのリージョンに配置し、それぞれNodeを一つ立ててその中にPodを複数用意して負荷分散するイメージです。InternetGateway、Ingress、NodePortを通してインターネット環境からPodにアクセスできるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243690/6655a26c-0bf4-9c9e-82b5-07f941a19e51.png)


## 用意するもの
- 開発環境
- AWSのクレデンシャル情報
  - AWS CLIを使用するため、アクセスキーとシークレットキーが必要になります
- 多少のお金
  - 確かAmazon EKSは無料枠の対象外だった気がするので、一応軽く調べてからの実行をお勧めします

## 準備
とりあえずサクッと準備を進めます。
AWSクレデンシャルを以下のように環境変数として設定することで、terraformを使ってAWSのリソースを操作できるようになります(※1)。

### 環境変数設定
```sh
# AWSクレデンシャルの設定
export AWS_ACCESS_KEY_ID=xxxxx
export AWS_SECRET_ACCESS_KEY=xxxxx
export AWS_DEFAULT_REGION=ap-northeast-1

# 各ツールのバージョン設定
export TERRAFORM_VERSION=1.3.3
export AWSCLI_VERSION=2.7.10
export TFLINT_VERSION=0.30.0
```

### 各種コマンドのインストール
terraform、aws-cli、kubectlをインストールしていきます。

```sh
# install terraform command
RUN curl -fsSL https://apt.releases.hashicorp.com/gpg | apt-key add - \
    && apt-add-repository "deb [arch=$(dpkg --print-architecture)] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \
    && apt-get -y install terraform=${TERRAFORM_VERSION}

# install aws-cli
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64-${AWSCLI_VERSION}.zip" -o "awscliv2.zip" \
    && unzip awscliv2.zip \
    && ./aws/install \
    && rm -rf ./aws ./awscliv2.zip

# install kubectl
RUN curl -o kubectl https://s3.us-west-2.amazonaws.com/amazon-eks/1.22.6/2022-03-09/bin/linux/amd64/kubectl \
    && chmod +x ./kubectl \
    && mkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$PATH:$HOME/bin \
    && echo 'export PATH=$PATH:$HOME/bin' >> ~/.bashrc \
    && kubectl version --short --client
```

### 資材作成
tfファイルとマニフェストファイルを作っていきます。Terraformでは、AWS上に展開するサービスはtfファイルを使って定義します。EKSクラスターもここで作ります。EKSクラスターを作った後で、マニフェストを使ってクラスター上にアプリを展開していきます。

#### ディレクトリ構成
Terraformに関しては理想的なディレクトリ構成のパターン([※2](https://dev.classmethod.jp/articles/terraform-bset-practice-jp/))がいくつかあるみたいですが、今回は一旦無視して以下の構成で資材を配置していきます。

```
.
├── terraform  // EKSクラスター環境をどかっと作るための資材
│   ├── backend.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── provider.tf
│   ├── variables.tf
│   └── terraform.tfvars
│
└── manifest  // EKSクラスター上にアプリを展開するための資材
    ├── cluster.yaml
    ├── ingress.yaml
    └── app.yaml
```

#### variables.tf
`variables.tf`で、terraformでサービスを構築する際に使う環境変数を宣言します。
環境変数の代入は`terraform.tfvars`で行います。
ここで定義した環境変数は、他のtfファイルで使用します。

```variables.tf
# provider用環境変数
variable "provider_region" {}

# VPC環境変数
variable "vpc_name" {}
variable "vpc_cidr" {}
variable "azs" {}
variable "public_subnets" {}
variable "private_subnets" {}

## EKS環境変数
variable "cluster_name" {}
variable "eks_managed_node_group_name" {}
variable "policy_aws_load_balancer_controller" {}
variable "role_aws_load_balancer_controller" {}
```

```terraform.tfvars
# provider用
provider_region = "ap-northeast-1"

# VPC用
vpc_name             = "vpc-tf-eks"
vpc_cidr             = "10.0.0.0/16"
azs                  = ["ap-northeast-1a", "ap-northeast-1c"]
public_subnets       = ["10.0.0.0/19", "10.0.32.0/19"]
private_subnets      = ["10.0.64.0/19", "10.0.96.0/19"]

# EKS用
cluster_name                        = "tf-eks-cluster"
eks_managed_node_group_name         = "tf-eks-managed-ng"
policy_aws_load_balancer_controller = "EKSIngressAWSLoadBalancerControllerPolicy"
role_aws_load_balancer_controller   = "EKSIngressAWSLoadBalancerControllerRole"
```

#### backend.tf
次に、terraformとterraform内で使用するproviderのバージョン条件を指定するtfファイルを作成します。
このファイルには`tfvars`で変数を定義できないので、直接書き込んでいきます(※)。

```backend.tf
terraform {
  # terraformのバージョン条件
  required_version = "~> 1.3.3"

  # 実行するproviderの条件
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.37.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~>2.14.0"
    }
  }
}
```

#### provider.tf
続いて、providerを定義していきます。
Terraformを使ってAWS上にサービスを展開する際には`aws`を、k8sを使用する際には`kubernetes`を定義する必要があるみたいです。
providerについては正直あまりイメージが湧いていないのですが、[こちらのサイト](https://qiita.com/ryysud/items/a38bcc856069801878e9)などが参考になるかもです。

```provider.tf
provider "aws" {
  region = var.provider_region
}

# EKSクラスタリソースを参照
data "aws_eks_cluster" "eks" {
  name = module.eks.cluster_id
}
data "aws_eks_cluster_auth" "eks" {
  name = module.eks.cluster_id
}

provider "kubernetes" {
  host                   = data.aws_eks_cluster.eks.endpoint
  cluster_ca_certificate = base64decode(data.aws_eks_cluster.eks.certificate_authority[0].data)
  token                  = data.aws_eks_cluster_auth.eks.token
}

```

#### main.tf
次に、AWS上でEKSクラスターを作成するための権限周りのリソースを定義していきます。
まず、事前にIAMポリシーの設定用ファイルをダウンロードする必要があるので、`terraform`ディレクトリに移動して以下のコマンドを実行します。
```sh
cd ./terraform
curl -o alb-ingress-policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.4/docs/install/iam_policy.json
```

権限周りのリソースとして以下の3つを定義します。
- Ingress Controller用のIAMポリシー
- ↑のポリシーを適用するIAMロール
- ↑のロールを使用するサービスアカウント

Ingress Controllerはk8s上のロードバランサ的なやつ(=Ingress)を管理する主体になります。Ingress Controllerにこのサービスアカウントを紐づけることで、Ingress ControllerはAWS上のリソースを使ってIngressを管理できるようになります。

```main.tf
resource "aws_iam_policy" "aws_loadbalancer_controller" {
  name   = var.policy_aws_load_balancer_controller
  policy = file("${path.module}/alb-ingress-policy.json")
}

module "iam_assumable_role_admin" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc"
  version = "~> 4.0"

  create_role                  = true
  role_name                    = var.role_aws_load_balancer_controller
  provider_url                 = replace(module.eks.cluster_oidc_issuer_url, "https://", "")
  role_policy_arns             = [aws_iam_policy.aws_loadbalancer_controller.arn]
  oidc_subjects_with_wildcards = ["system:serviceaccount:*:*"]
}

resource "kubernetes_service_account" "aws_loadbalancer_controller" {
  metadata {
    name      = "aws-load-balancer-controller"
    namespace = "kube-system"
    annotations = {
      "eks.amazonaws.com/role-arn" = module.iam_assumable_role_admin.iam_role_arn
    }
  }
}
```

次に、VPCを定義していきます。変数周りは`tfvars`ファイルで定義しているのでそれを使います。
一点注意しないといけないのは、これはterraformに限った話ではないのですが、EKSでk8s環境を構築する場合、以下のようにサブネットのタグに`"kubernetes.io/role/elb" = "1"`と設定する必要があります。これを設定することで、Ingress Controllerがどのサブネットを使用してk8s環境を構築するのかを自動で見分けて動いてくれます。
```main.tf
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~>3.18.0"

  name = var.vpc_name
  cidr = var.vpc_cidr

  azs             = var.azs
  public_subnets  = var.public_subnets
  private_subnets = var.private_subnets

  public_subnet_tags = {
    "kubernetes.io/role/elb" = "1"
  }

  enable_nat_gateway = true
  single_nat_gateway = false
  enable_vpn_gateway = false
}
```

次に、EKSクラスターを作成していきます。こちらも基本的に`tfvars`ファイルで設定した変数や、上で定義したVPCに紐づく値を使って定義します。
```main.tf
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~>18.0"

  cluster_name    = var.cluster_name
  cluster_version = "1.22"

  cluster_endpoint_private_access = true
  cluster_endpoint_public_access  = true

  vpc_id      = module.vpc.vpc_id
  subnet_ids  = module.vpc.private_subnets
  enable_irsa = true
  eks_managed_node_groups = {
    "${var.eks_managed_node_group_name}" = {
      desired_size   = 2
      instance_types = ["t3.small"]
    }
  }

  node_security_group_additional_rules = {
    # AdmissionWebhookが動作しないので追加指定
    admission_webhook = {
      description                   = "Admission Webhook"
      protocol                      = "tcp"
      from_port                     = 0
      to_port                       = 65535
      type                          = "ingress"
      source_cluster_security_group = true
    }
    # Node間通信を許可
    ingress_mode_communications = {
      description = "Ingress Node to Node"
      protocol    = "tcp"
      from_port   = 0
      to_port     = 65535
      type        = "ingress"
      self        = true
    }
    egress_node_communications = {
      description = "Egress Node to Node"
      protocol    = "tcp"
      from_port   = 0
      to_port     = 65535
      type        = "egress"
      self        = true
    }
    # Node -> Localの通信を許可
    egress_node_to_local = {
      description = "Egress Node to Local"
      type        = "egress"
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = [module.vpc.vpc_cidr_block]
    }
  }
}
```

#### outputs.tf
最後に、作成したEKSクラスターを利用するための「設定ファイル」の出力を定義します。
```outputs.tf
output "aws_auth_config_map" {
  value = module.eks.aws_auth_configmap_yaml
}
```

ここまで資材を作成したら、以下のコマンドを実行してどかっと環境を構築できます。
構築完了までには10分程度かかります。
```sh
# 初期化
terraform init

# 環境構築
terraform apply
```


# クラスター上へアプリケーションを展開
ここまででTerraformを使ったEKSクラスター構築は完了しました！
ここからは、クラスター上にアプリケーションを展開していきます。

まず`manifest`ディレクトリに移動して、outputs.tfで定義した出力からマニフェストファイルを作成します。
```sh
cd ../manifest
terraform output aws_auth_config_map > ./manifest/aws-auth-configmap.yaml
```


以下を参考にしながら↑で作成したファイルを編集し、k8sの操作を許可したい任意のIAMユーザを追加します。
（先頭・末尾のEOTは削除してください）
いない場合、この作業は不要になります。
```aws-auth-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-auth
  namespace: kube-system
data:
  mapRoles: |
    - rolearn: arn:aws:iam::xxxxxxxxxxx:role/tf-eks-node-group-xxxxxxxxxxxxxxxx
      username: system:node:{{EC2PrivateDNSName}}
      groups:
        - system:bootstrappers
        - system:nodes
  # 以下を追加
  mapUsers: |
    - userarn: arn:aws:iam::xxxxxxxxxxx:user/xxxxxx
      username: xxxxxx
      groups:
        - system:administrator
```

ユーザを追加できたら、以下のコマンドを実行します。
ここからはkubectlを使ってk8s環境を操作していくので、まずはkubectlで扱うクラスターの設定を更新します。
```sh
# kubectlで扱うクラスターの設定更新
export CLUSTER_NAME=tf-eks-cluster
aws eks update-kubeconfig --name ${CLUSTER_NAME}
```

設定を更新したら、↑で作成したマニフェストを適用してterraform以外の一般ユーザにもEKSクラスターを操作できるようにします。例のごとく、マニフェストを作成していない場合はこのコマンドは実行不要です。
```sh
# Terraform以外の一般ユーザがEKSへ接続できるようにする
kubectl apply -f aws-auth-configmal.yaml
```

cert-managerについては正直完全にノータッチだったので、他の素晴らしい記事に解説をお譲りします。
```sh
# cert-manager作成
kubectl apply \
    --validate=false \
    -f https://github.com/jetstack/cert-manager/releases/download/v1.5.4/cert-manager.yaml
```

次に、Ingress Controllerを作ります。マニフェストファイルはGitHubからコピってきていい感じに修正したら、クラスターに適用すれば作成できます。
```sh
# Ingress Controller用マニフェスト作成
curl -Lo ingress-controller.yaml https://github.com/kubernetes-sigs/aws-load-balancer-controller/releases/download/v2.4.4/v2_4_4_full.yaml
sed -i.bak -e '480,488d' ./ingress-controller.yaml
sed -i.bak -e 's|your-cluster-name|${CLUSTER_NAME}|' ./ingress-controller.yaml
curl -Lo ingclass.yaml https://github.com/kubernetes-sigs/aws-load-balancer-controller/releases/download/v2.4.4/v2_4_4_ingclass.yaml

# Ingress Controller作成
kubectl apply -f ingress-controller.yaml
kubectl apply -f ingclass.yaml
```

次に、Ingress用のマニフェストを作成します。
```ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
  alb.ingress.kubernetes.io/scheme: internet-facing
  name: ingress-tf-eks
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - backend:
              service:
                name: web-tf-eks
                port:
                  number: 80
            path: /
            pathType: Prefix
```

マニフェストからIngressを作成します。
```sh
# Ingress作成
kubectl apply -f ingress.yaml
```

最後に、アプリケーション用コンテナを配置するマニフェストを作成します。ここではアプリケーションを実行してくれるコンテナ用イメージか、イメージが格納されたリポジトリを指定する必要があります。今回はサンプルとして、nginxのイメージを指定します。
NodePortの設定も併せて同じファイルに記述します。
```app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web-tf-eks
  name: web-tf-eks
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-tf-eks
  strategy: {}
  template:
    metadata:
      labels:
        app: web-tf-eks
    spec:
      containers:
        - image: nginx:latest
          name: web-tf-eks
---
apiVersion: v1
kind: Service
metadata:
  name: web-tf-eks
spec:
  ports:
    - name: 80-80
      port: 80
      protocol: TCP
      targetPort: 80
  selector:
    app: web-tf-eks
  type: NodePort
```

マニフェストからnginxコンテナを作成します。
```sh
# アプリケーション作成
kubectl apply -f app.yaml
```

お疲れ様でした！これでk8s環境にアプリケーションを展開することができました！
ここまでに作成したリソースの状態を確認してみましょう。
```sh
# 確認
kubectl get pod,svc,ingress
```

↓こんな感じで、PodやService、Ingressなどの状態が確認できます。
Podは2つともSTATUSがRunningということで、正常に稼働していることがわかります。
![スクリーンショット 2022-12-15 4.56.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243690/df93c1ab-84cc-8f13-b0b8-e8c15e0ad92a.png)

これでアプリケーションの準備が整ったので、早速アクセスしてみましょう！
上で確認したIngressの状態から、ADRESS欄に表示されるアドレスをブラウザに打ち込んでアクセスできます。
実際にアクセスすると、

![nginx.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243690/a8eafe9e-2472-b77b-7255-f6912a17c52f.png)

こんな感じでnginxのページが表示されているのが確認できます！

以上が今回の内容になります。
最後に、ここまでに立ち上げたサービスを全て削除するのを忘れないようにしましょう！

```sh
# アプリケーションを削除
kubectl delete -f app.yaml

# Ingressを削除
kubectl delete -f ingress.yaml

# Ingress Controllerを削除
kubectl delete -f ingress-controller.yaml

# EKS環境を削除
cd ../terraform/
terraform destroy
```

# まとめ
今回はTerraformとAmazon EKSを使ってk8s環境を構築しました！
構築したシステム自体はかなり控えめだったのでTerraformやk8sの恩恵を感じるところまでは至らなかったかもしれませんが、これらの技術に興味を持つ足掛かりとなれば幸いです。

# 参考・お役立ちサイト
- ※1: 他にもクレデンシャルの設定方法はあります。参考: https://qiita.com/Hikosaburou/items/1d3765d85d5398e3763f
- ※2: 参考: https://dev.classmethod.jp/articles/terraform-bset-practice-jp/
- ※3: 直接書き込む以外にも一応方法はあります。参考: https://qiita.com/ymmy02/items/e7368abd8e3dafbc5c52）