【EC2】【kubeadm】様々なツールを使ってk8sを導入した(所要1時間)

# はじめに

様々なツールを使ってEC2上へk8sを導入しました。
以下に手順(`README.md`)、コードをアップロードしています。

<https://github.com/not75743/k8s_madeby_packer_terraform_ansible_kubeadm>

~1時間で作成可能であるため、k8sを試して見たい方は覗いてみてください。

今回の記事では手順作成でつまずいた箇所、注目すべき箇所をメモしたいと思います。

※本投稿では手順の詳細を記載しません。
詳細については上記のリンクより`README.md`を参照ください。

## 作れるもの

- k8sクラスタ
  - master *1
  - worker *1~3

構成図は以下となります。

![EC2_kubernetes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1464314/bfbf5aed-b1cc-e086-b360-b092bbcd1065.png)

## 対象読者

- とりあえずk8sを試してみたい。
- ローカル環境にサーバがないから利用したい。
- ツール(packer,ansible,terraform)の使い方について参考にしたい

# クラスタ作成の流れ

ざっくり以下になります

1. 環境を用意する
2. packerでk8sノードのイメージ作成
3. terraformでAWS環境にリソース作成
4. ansibleでmasterノード用意
5. ansibleでworkerノード用意

※ 3~5はシェルスクリプトを使ってほぼ自動で実施するようにしています。

この項目ごとにポイントをピックアップしていきたいと思います。
今回はkubeadmでクラスタを用意しており、各手順で以下を参考にしました。

<https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/>
<https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/>

## 1.環境を用意する

もろもろのツールをインストールします。すでにあれば不要です。

### dockerfile

dockerfileを用意しているため、用意に環境を準備可能です。
centos7をベースイメージとしています。

## 2.packerでk8sノードのイメージ作成

master,workerの共通AMIを作成するためにpackerを使用します。
AMIを作っておくことで2回目以降のクラスタ作成時間を短縮することが可能です。
AMI構築にはAnsible Remote Rrovisionerを利用しました。
<https://www.packer.io/docs/provisioners/ansible/ansible>

### AWS用のクレデンシャルが不足する

packer build実行時、以下の様な出力が出ました。

```bash
no valid credential sources for found
```

`no valid credential`とある通り、AWS用のcredentialが読み込めていません。
ローカルにcredentialを残さないため、今回は環境変数でcredentialを設定します。

```bash
export AWS_ACCESS_KEY_ID="xxxxxxxxxxxxxxxxxx"
export AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxxxxxxxx"
```

参考は以下
<https://www.packer.io/docs/builders/amazon#authentication>

### sftpエラーとなる

packer build実行中にansibleが実行できなかった旨のログが出力されました。

```bash
amazon-ebs.centos: fatal: [kubenode]: FAILED! => {"msg": "failed to open a SFTP connection (EOF during negotiation)"}
```

Ansible Provisionerはリモートサーバ上で`/usr/lib/sftp-server -e`を実行し、ローカルとsftpでやり取りします。
CentOS7のベースAMIでは上記パスにsftpが存在しないため、以下の様にパスを書き換える必要があります。

```hcl:main.pkr.hcl
sftp_command = "/usr/libexec/openssh/sftp-server -e"
```

参考は以下
<https://www.packer.io/docs/provisioners/ansible/ansible#sftp_command>
<https://www.packer.io/docs/provisioners/ansible/ansible#redhat-centos>

### packer -v が出力されない

packer -v を実行してもバージョンが出力されず、出力待ちでプロンプトが停止しました。
原因は以下のpackerを実行しているからでした

```bash
/usr/sbin/packer
```

実態は`cracklib-packer`というパスワード正常性チェッカーでした。
今回は使わないのでdockerfileビルド時にシンボリックリンクを削除しています。

```bash
$ ll /usr/sbin/packer
lrwxrwxrwx. 1 root root 15 12月 29  2020 /usr/sbin/packer -> cracklib-packer
```

参考は以下
<https://qiita.com/0xmks/items/2513745d927941869d64>
<https://www.kabegiwablog.com/entry/2018/02/02/090000>
<https://github.com/cracklib/cracklib/issues/7>

## 3.terraformでAWS環境にリソース作成

k8sノードを含めたAWSリソース作成にterraformを使用しました。
terraformを使うことで容易な環境作成、破棄が可能となります。

### outputをansibleのhostsファイルに記載

（正確にはterraformでなくshellですが）本手順ではEC2のpublic ipを取得するために`main.tf`内でoutputを設定し、それを各ファイルにsedで転記しています。
「sedがipアドレス追記に使える！」と思いスクリプトに反映させましたが、調べてみるとほかにも手段がありそうなので、今後調べてみたいです。
[EC2 inventory source](https://docs.ansible.com/ansible/latest/collections/amazon/aws/aws_ec2_inventory.html)
[PRODUCE AN ANSIBLE INVENTORY WITH TERRAFORM](https://www.linkbynet.com/produce-an-ansible-inventory-with-terraform)

### セキュリティグループは指定IPのみ許可

セキュリティグループは以下の様に接続元IPを記載したIPのみに絞ります。

```hcl
resource "aws_security_group_rule" "worker_ssh" {
  type              = "ingress"
  from_port         = 22
  to_port           = 22
  protocol          = "tcp"
  cidr_blocks       = var.ssh_to_worker
  security_group_id = aws_security_group.kube_worker-sg.id # ここ
}
```

`terraform.tfvars`でホストのIPを忘れずに記載しましょう。

### ノードの数を指定

workerのEC2の個数を1~3にするために以下のようにコードを書きました。

```hcl
resource "aws_instance" "worker" {
  count                       = (var.worker_count == 0 ? 1 : (var.worker_count < 4 ? var.worker_count : 3)) # ここ
```

いわゆるif文です。
入れ子にし、0,4以上を選択した場合にそれぞれ1,3に置き換える様にしています。
0だとk8sの意味がなくなる、4以上は結構な課金がされる、ということを考慮して書きました。

見づらい（気がする？）ため、もしほかにやり方が合ったらご教授いただけると幸いです。

参考は以下
<https://www.terraform.io/docs/language/expressions/conditionals.html>

## 4.ansibleでmasterノード用意

terraformで用意したノードにSSH接続し、ansibleで`kubeadm init`を実行します。

### --apiserver-cert-extra-sans={{ masterip }}

k8sのapiserverにSAN(Subject Alternative Name)を追加します。
オプションなし`kubeadm init`で生成されるapiserver証明書のSANにはバブリックIPの記載がなく（CommonNameにもなく）、バブリックIP指定での接続が不可であるため本設定を追加します。

```bash
[root@ip-10-0-1-100 ~]# openssl x509 -text -noout -in /etc/kubernetes/pki/apiserver.crt | grep -A1 "X509v3 Subject Alternative Name:"
            X509v3 Subject Alternative Name:
                DNS:ip-10-0-1-100.ap-northeast-1.compute.internal, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:10.96.0.1, IP Address:10.0.1.100, IP Address:3.113.29.168 # ここの一番後ろに追加されている
```

参考は以下
<https://stackoverflow.com/questions/46360361/invalid-x509-certificate-for-kubernetes-master>
<https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-init/>
<https://kubernetes.io/ja/docs/setup/best-practices/certificates/>

### get token & token-ca-cert-hash

それぞれworkerがk8sに参加するkubeadm join 時に必要になります。
masterのplaybook実行時にそれぞれ拾い、それを`lineinfile`モジュールで変数ファイルに記載し、workerのplaybookで参照できるようにします。

```yaml:master.yaml
    - name: get token
      shell: kubeadm token list | awk 'NR==2{print $1}'
      register: token

    - name: replace worker var "token"
      lineinfile:
        path: ./vars/vars.yaml
        regexp: "^token:"
        line: "token: {{ token.stdout }} "
        state: present
      delegate_to: localhost
      become: no
```

`lineinfile`はローカルで実行するため`delegate_to`でローカルを選択します。

参考は以下
<https://qiita.com/fumiya-konno/items/d2d6b67296e5ee94340b>
<https://docs.ansible.com/ansible/latest/user_guide/playbooks_delegation.html#id3>

### kubeconfig作成

k8sのAPIサーバと通信するためにkubeconfigをローカルに持ってきます。
持っていく先は~/.kube/configにすると、kubectl時に自動で反映されるため楽です。
すでにk8sを使っているユーザの場合、既存kubeconfigを上書きしてしまうと困るため、今回は作業ディレクトリ内にコピーし、そのパスを環境変数`KUBECONFIG`で指定するようにしています。

参考は以下
<https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/_print/>
<https://kubernetes.io/ja/docs/concepts/configuration/organize-cluster-access-kubeconfig/#kubeconfig%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0>

## 5.ansibleでworkerノード用意

masterのplaybookが終了したらworkerのplaybookを実行します。
とはいってもkubeadm joinを実行するだけです。

### playbook実行時の「Are you sure you want to continue connecting」を防ぐ

masterもworkerも初めてSSH接続するノードであるため、playbook実行前に接続するか問われます。

```bash
The authenticity of host 'xxxx.com (xxx.xxx.xxx.xxx)' can't be established.
RSA key fingerprint is xx:xx:xx:xx...
Are you sure you want to continue connecting (yes/no)?
```

ansibleで接続できなくなるため、cfgファイルに`host_key_checking = False`を記載し、playbook実行前に環境変数で指定します。

```bash
export ANSIBLE_CONFIG=./cfg/ansible.cfg
```

```yaml:ansible.cfg
[defaults]
host_key_checking = False # to avoid ssh check
```

参考は以下
<https://docs.ansible.com/ansible/latest/reference_appendices/config.html#host-key-checking>

# おわりに

大変でしたが、1から作って各コンポーネントの理解を深められたと思います。
以下の様な課題がまだあるため、これから手を付けていきたいところです。

- packer,terraform,ansibleでファイル、ディレクトリ構成をあまり意識していなかった(roleで分ける等)
  
- kubeadm initで何しているのか追い切れていない。

- terraform destroy を忘れると課金が止まらない。(監視の仕組みを作りたい)