TerraformでEC2に勉強用Kubernetes、Docker環境をさっと作成する

# はじめに

最近、勉強用にKubernetes、Docker環境を使いたいことが多くあります。
コンテナを動かすサーバーはそれなりのスペックが必要なのでお金がかかってしまいます。
そのため、Terraformで楽に環境を作りたいと思います。

# 作成する構成

EC2を作成して、クラスタを作ります。ClientからSSH接続してEC2内部でクラスタにアクセスするようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/15fe82d4-b7b1-cecf-4679-56c9a92a0d73.png)

# 作業の流れ

もし手作業で１つ１つ作る場合は以下の流れになります。
1. EC2を配置するネットワーク（VPC,サブネット）を作成する
勉強用にネットワークセキュリティグループでアクセス制限したうえで作成したいです。しかし、クライアントのグローバルIPを調べたり覚えておくのは面倒です。
1. SSH接続のためのキーペアを作成する
事前に作成しておいたキーを使ってキーペアを作成します。
1. EC2を作成する
シングルノードで作成します。SSH接続するにはEC2のグローバルIPを知らないといけませんが、AWSコンソールにアクセスして確認するのが面倒です。
1. Kubernetes、Dockerをインストールする
勉強の目的によってKubernetesは使わない場合があります。共通部分はモジュール化してインストール方法を個別で指定できるようにしたいです。

一連の流れをTerraformで行うようにしたいです。

# 実践する

## ネットワークを作成するtfファイル

```terraform:network.tf
data "http" "client_global_ip" {
  url = "https://ifconfig.co/ip"
}
locals {
  allowed_cidr = replace("${data.http.client_global_ip.response_body}/32","\n","")
}
resource "aws_vpc" "test_vpc" {
  cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "test_subnet" {
  vpc_id = aws_vpc.test_vpc.id
  map_public_ip_on_launch = true
  cidr_block = "10.0.0.0/24"
}
resource "aws_internet_gateway" "test_igw" {
  vpc_id = aws_vpc.test_vpc.id
}
resource "aws_route_table" "test_rt" {
  vpc_id = aws_vpc.test_vpc.id
  route {
      cidr_block = "0.0.0.0/0"
      gateway_id = aws_internet_gateway.test_igw.id
  }
}
resource "aws_route_table_association" "test_rta" {
  subnet_id = aws_subnet.test_subnet.id
  route_table_id = aws_route_table.test_rt.id
}
resource "aws_security_group" "test_sg" {
  vpc_id = aws_vpc.test_vpc.id
  ingress {
    from_port = 22
    to_port = 22
    protocol = "tcp"
    cidr_blocks = [local.allowed_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

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

|リソース種類|説明|
|---|---|
|aws_vpc|EC2を配置するVPC|
|aws_subnet|EC2を配置するサブネット。配置するインスタンスにグローバルIPを付与するようにしています。|
|aws_internet_gateway|インターネットにアクセスするためのゲートウェイ|
|aws_route_table|ネットワークの経路を定義するルートテーブル|
|aws_route_table_association|ルートテーブルとサブネットを紐づける設定|
|aws_security_group|ネットワークへのアクセス制限をするセキュリティグループ。クライアントのグローバルIPを自動取得して設定するようにしています。|

※参考

https://dev.classmethod.jp/articles/reference-my-pubic-ip-in-terraform/

https://qiita.com/TBT/items/1e87604293842efa0743

https://qiita.com/koki_develop/items/45cdde3d27bd75f1bfd5

## キーペアを作成するtfファイル

```terraform:keypair.tf
resource "aws_key_pair" "test_kp" {
  public_key = file(var.instance_public_key)
}
```

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

|リソース種類|説明|
|---|---|
|aws_key_pair|SSH接続に使うキーペア。変数に公開鍵のパスを入れて、file()で読み込んでpublic_keyに渡すようにしています。|

## EC2を作成するtfファイル

```terraform:server.tf
data "aws_ssm_parameter" "ami" {
  name = "${var.ami_name}"
}
resource "aws_network_interface" "test_ni"{
  subnet_id = aws_subnet.test_subnet.id
  security_groups = [aws_security_group.test_sg.id]
}
resource "aws_instance" "test_instance" {
  ami = data.aws_ssm_parameter.ami.value
  instance_type = "${var.instance_type}"
  key_name = aws_key_pair.test_kp.id
  network_interface {
    network_interface_id = aws_network_interface.test_ni.id
    device_index = 0
  }
  root_block_device {
    volume_size = ${var.root_block_volume_size}
  }
}
```

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

|リソース種類|説明|
|---|---|
|aws_network_interface|EC2に設定するネットワークインターフェース。|
|aws_instance|クラスタを作成するEC2。最新のAMIイメージを取得して起動するようにしています。|

※参考

https://dev.classmethod.jp/articles/launch-ec2-from-latest-amazon-linux2-ami-by-terraform/

## Kubernetes、Dockerをインストールするtfファイル

勉強の目的によりインストール内容、方法が異なり、tfファイルを使い分ける必要が出てきます。
今回はKubernetesを使う場合について記載します。

```terraform:initialize-server.tf
resource "null_resource" "init_ec2" {
  triggers = {
    public_ip = "${module.common_resources.test_instance.public_ip}"
  }

  connection {
    type = "ssh"
    user = "ec2-user"
    host = self.triggers.public_ip
    private_key = file(module.common_resources.instance_private_key)
  }

  provisioner "remote-exec" {
    script = "../scripts/install-docker.sh"
  }
  provisioner "remote-exec" {
    script = "../scripts/install-kubernetes.sh"
  }
}
```

今回はあまり関係ありませんが、remote-execを分けると再ログイン扱いとなり、グループ変更などを反映できて便利だと思いました。

```bash:install-docker.sh
#!/bin/bash -ex

echo 'install docker'
VERSION="20.10.17"
sudo yum install -y docker-${VERSION}
sudo systemctl start docker
sudo usermod -aG docker $(whoami)
```

```bash:install-kubernetes.sh
#!/bin/bash -ex

echo "install kubernetes"

echo "`ip -4 a show eth0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'` `hostname`" | sudo tee -a /etc/hosts
cat << EOF | sudo tee -a /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=0
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF

#sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
VERSION="1.26.0"
sudo yum install -y kubelet-${VERSION} kubeadm-${VERSION} kubectl-${VERSION} --disableexcludes=kubernetes
sudo swapoff -a
sudo systemctl start kubelet
sudo systemctl enable kubelet
cat << EOF | sudo tee /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl restart kubelet
sudo kubeadm init

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

echo 'setup master node'
kubectl apply -f https://github.com/weaveworks/weave/releases/download/v2.8.1/weave-daemonset-k8s.yaml
CONTROL_PLANE=$(kubectl get node | grep control-plane | awk '{print $1}')
kubectl taint nodes $CONTROL_PLANE node-role.kubernetes.io/control-plane:NoSchedule-

echo 'setup kubectl'
echo 'alias k="kubectl"' >> ~/.bash_profile
```

また、プロバイダーを定義したtfファイルを作成します。

```terraform:terraform.tf
terraform {
  required_providers {
    aws = {
      source = "registry.terraform.io/hashicorp/aws"
      version = "4.47.0"
    }
    http = {
      source = "registry.terraform.io/hashicorp/http"
      version = "3.2.1"
    }
    null = {
      source = "registry.terraform.io/hashicorp/null"
      version = "3.2.1"
    }
  }
}

provider "aws" {
（略）
}
provider "http" {}
provider "null" {}

module "common_resources" {
  source = "../common"
}
```

## init,applyして動いていることを確認する

細かいtfファイルの内容は省きます。以下のように作成したファイルを配置します。

```bash
$ pwd;find . | sort | sed '1d;s/^\.//;s/\/\([^/]*\)$/|--\1/;s/\/[^/|]*/|  /g'
/home/ari_sangu/qiita/99.tools/tf-with-ec2
|--common #AWSのリソースを定義したモジュール
|  |--keypair.tf
|  |--network.tf
|  |--output.tf
|  |--server.tf
|  |--terraform.tf
|  |--variables.tf
|--kubernetes #Kubernetesクラスタを作成するファイル
|  |--initialize-server.tf
|  |--output.tf
|  |--terraform.tf
|--scripts #クラスタを作成するためのスクリプト
|  |--install-docker.sh
|  |--install-kubernetes.sh
```

それではterraformコマンドを実行していきます。

```bash
$ cd kubernetes/
$ terraform init
```

また、AWSのCLIを実行するには認証が必要なのですが、今回はローカルに認証ファイルを置きたくなかったので環境変数で設定しました。

```bash
$ source ../../aws-cli-source.sh
```

```bash:aws-cli-source.sh
export AWS_ACCESS_KEY_ID='...'
export AWS_SECRET_ACCESS_KEY='...'
export AWS_REGION='...'
```

https://registry.terraform.io/providers/hashicorp/aws/latest/docs

applyします。

```bash
$ terraform apply
・・・
Outputs:

ssh_cmd = "ssh -i <key> ec2-user@18.183.62.67"
```

Terraformでは上記のようにEC2のグローバルIPをOutputsとして表示することができます。
output.tfに以下のように書いてます。

```terraform
output "ssh_cmd" {
  description = "to access ec2 by ssh"
  value = "ssh -i ${var.instance_private_key} ec2-user@${aws_instance.test_instance.public_ip}"
}
```

無事クラスタができるとSSH接続後、kubectlコマンドが使えるようになっています。

```bash
[ec2-user@ip-10-0-0-90 ~]$ k version
WARNING: This version information is deprecated and will be replaced with the output from kubectl version --short.  Use --output=yaml|json to get the full version.
Client Version: version.Info{Major:"1", Minor:"26", GitVersion:"v1.26.0", GitCommit:"b46a3f887ca979b1a5d14fd39cb1af43e7e5d12d", GitTreeState:"clean", BuildDate:"2022-12-08T19:58:30Z", GoVersion:"go1.19.4", Compiler:"gc", Platform:"linux/amd64"}
Kustomize Version: v4.5.7
Server Version: version.Info{Major:"1", Minor:"26", GitVersion:"v1.26.0", GitCommit:"b46a3f887ca979b1a5d14fd39cb1af43e7e5d12d", GitTreeState:"clean", BuildDate:"2022-12-08T19:51:45Z", GoVersion:"go1.19.4", Compiler:"gc", Platform:"linux/amd64"}
```

## 作ったリソースを削除する

放置すると高額コストになるので、使い終わったら削除します。

```bash
$ terraform destroy
・・・
Destroy complete! Resources: 10 destroyed.
```

# おわりに

今まで手動でやっていたものをコマンド一つでできるようにしてみました。
初めはコマンドに慣れていることもあって恩恵を感じにくかったのですが、少しコマンドを忘れた時に思い出す量が少なくてよいと思いました。
ただ、今まで手動で行っていたことをtfファイルに起こす必要があります。書き方は勉強して管理しやすくしていく必要があると感じました。
これからも勉強していきます。