Amazon EKSを使わずにk8sクラスタを立ててみた話


そうだ、k8sクラスタを立てよう

時は2023年12月21日、突如としてTLの読み込みができなくなるX(旧Twitter)。
幸いにしてすぐ復旧はなされたものの、X(旧Twitter)以外のSNSへの移行を検討し始めるには十分な出来事であった…

有力な移行先としてmisskey.ioが頭によぎったものの、せっかくの分散型SNSであるならば自前でmisskey鯖立ててみるのも一興である。ちょろっと調べてみた感じコンテナイメージとして提供されているようだったので、であるならば一興ついでにKubernetes(k8s)環境も構築してみるのも面白そうである。

どのサービスで立てよう…?

AWSアカウントを持っていることもあり、とりあえずお試しで立ててみる分にはAWSで立ててみるのが良さそうか…?と考えた筆者。
Amazon EKSを利用すればコマンド一発でk8sクラスタを払い出すことも可能で、あとはアプリケーションをデプロイするだけというのは楽そうである。

しかしながら、EKS自体に利用料がかかることやスポットインスタンスを利用したk8sクラスタが払い出せるかがイマイチ不明瞭なこともあり、さっと試すには不向きだなとなった。

EC2だけでどうにかする方法がないものかと調べてみたところ、それっぽいページを見つけることができた。

AWS環境での構築ログではないが、これを参考にEC2スポットインスタンス上にk8sクラスタを構築することはできそうである。

やってみよう

前提条件

  • WSL2 on Windows 11 から操作

    • terraform、ansibleがインストール済み

  • AWS Web コンソール上にてIAMユーザ作成済み

    • ssh接続に必要な秘密鍵ダウンロード済み

    • アクセスキー、シークレットキー作成済み

  • AWS Web コンソール上にてVPC作成済み

セキュリティグループ準備

k8s公式ページを参考に、各種ポートで通信ができるようセキュリティグループを作成しておく。control-plane用とworker用に分けて作っておくのが良い。

また、ec2インスタンスにssh接続して各種設定を行えるよう22番ポートで通信可能にするセキュリティグループも作成しておくこと。

ec2インスタンス起動

以下設定でcontrol-plane用とworker用でそれぞれ起動する。

  • image: ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20231207

  • flavor: t3a.2xlarge

  • ストレージサイズ: 30GB以上

  • サブネットは作成済みVPCに含まれるものを指定

  • キーペア、セキュリティグループは作成済みのものを指定

お手本に従ってコマンド実施

https://blog.estampie.work/archives/2585  の手順に従って構築を進める。
IPアドレスを書き換えるところはパブリックIPアドレスを指定することで対応。

問題発生…

手順通りにcontrol-plane nodeおよびworker nodeがセットアップできたので、https://cstoku.dev/posts/2018/k8sdojo-09/  を参考にしてnginxをデプロイしてみたところ、全然繋がらない…
何かエラー吐いてないかとPodのログを見てみたところ、以下エラーが出力されていた。

Error from server: no preferred addresses found; known addresses: []

おもむろにエラーメッセージでググってみたところ、どうやらworker nodeにIPアドレスが割り当てられてない時に出るエラーらしい。worker nodeの10-kubeadm.conf にはプライベートIPアドレスを指定する必要がありそうだ。

試行錯誤を簡単に繰り返せるようにしよう

ところで、筆者は利用料金節約のためにec2インスタンスを起動させっぱなしにすることはなく毎回律儀にシャットダウンするようにしている。そうすると、ec2インスタンスを立て直す度セットアップ作業を行う必要に駆られていた。
さすがにめんどくなってきたので、ec2インスタンス起動時にある程度セットアップも済ませるようにしたい。

terraformおよびansibleによるIaC化

kubernetes.tf 作成

provider "aws" {
  region = "ap-northeast-1"
  access_key = "*******"
  secret_key = "*******"
}

# image
data "aws_ami" "ubuntu_22_04" {
  owners      = ["amazon"]
  most_recent = true

  filter {
    name   = "name"
    values = ["*ubuntu-jammy-22.04-amd64-server*"]
  }
}

# instance
resource "aws_instance" "master_node" {
  instance_type = "t3a.2xlarge"
  ami = data.aws_ami.ubuntu_22_04.id
  key_name = "hogehoge"

  instance_market_options {
    market_type = "spot"
    spot_options {
      max_price = 0.710
    }
  }
  vpc_security_group_ids = [
    "sg-hoge", 
    "sg-fuga"
  ]
  subnet_id = "subnet-hogefuga"

  root_block_device {
    volume_size = 30
    volume_type = "gp3"
  }

  lifecycle {
    ignore_changes = [
      ami
    ]
  }

  provisioner "local-exec" {
    command = "ansible-playbook -i ${self.public_dns}, ~/ansible/setup_k8s.yaml"
  }
}

resource "aws_instance" "worker_node" {
  instance_type = "t3a.2xlarge"
  ami = data.aws_ami.ubuntu_22_04.id
  key_name = "hogehoge"

  instance_market_options {
    market_type = "spot"
    spot_options {
      max_price = 0.710
    }
  }

  vpc_security_group_ids = [
    "sg-hoge", 
    "sg-fuga"
  ]
  subnet_id = "subnet-hogefuga"

  root_block_device {
    volume_size = 30
    volume_type = "gp3"
  }

  lifecycle {
    ignore_changes = [
      ami
    ]
  }

  provisioner "local-exec" {
    command = "./check_ssh_connection.sh ${self.public_dns}"
  }

  provisioner "local-exec" {
    command = "ansible-playbook -i ${self.public_dns}, ~/ansible/setup_k8s.yaml"
  }
}


# display public DNS name
output "public_dns_master_node" {
  value = aws_instance.master_node.public_dns
}

output "public_dns_worker_node" {
  value = aws_instance.worker_node.public_dns
}

こんな感じのテキストファイルを用意する。拡張子はtf。
access_key, secret_keyなどは作成済みのものを指定する。

インスタンス作成後に実施したい処理はprovisionerを指定することでterraformに実施させることが可能。
注意点として、ec2インスタンス起動からssh可能になるまではタイムラグがあるため、ansible実行を何らかの方法で遅らせる必要がある。

playbook 作成

ec2インスタンス起動後に行うセットアップ処理をyamlファイルに定義したものを準備する。
今回準備したファイルの一覧は以下。

~/ansible/
├── roles
│   ├── containerd
│   │   ├── files
│   │   │   ├── config.toml
│   │   │   ├── k8s.conf
│   │   │   ├── kubelet
│   │   │   └── sysctl.conf
│   │   └── tasks
│   │       └── main.yaml
│   ├── docker
│   │   └── tasks
│   │       └── main.yaml
│   └── kubernetes
│       └── tasks
│           └── main.yaml
└── setup_k8s.yaml

各yamlの中身は以下の通り。

# setup_k8s.yaml
- hosts: all
  become: yes
  vars:
    ansible_python_interpreter: /usr/bin/python3
  roles:
    - kubernetes
    - docker
    - containerd
# roles/containerd/tasks/main.yaml
- name: copy k8s.conf
  copy:
    src: k8s.conf
    dest: /etc/modules-load.d/k8s.conf

- name: modprobe k8s config
  shell: |
    modprobe overlay
    modprobe br_netfilter 

- name: copy sysctl conf
  copy:
    src: sysctl.conf
    dest: /etc/sysctl.d/k8s.conf

- name: reload sysctl config
  command: sysctl --system

- name: copy containerd config
  copy:
    src: config.toml
    dest: /etc/containerd/config.toml

- name: reload containerd
  systemd:
    name: containerd
    state: restarted

- name: copy kubelet config
  copy:
    src: kubelet
    dest: /etc/sysconfig/kubelet

- name: reload kubelet
  systemd:
    name: kubelet
    daemon_reload: yes
    state: restarted
# roles/docker/tasks/main.yaml
- name: apt update
  command: apt-get update

- name: install package
  apt:
    name:
      - ca-certificates
      - curl
      - gnupg
    update_cache: yes
    state: present

- name: insatall keyrings
  command: install -m 0755 -d /etc/apt/keyrings

- name: download ubuntu official gpg key
  shell: curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg

- name: add permission
  command: chmod a+r /etc/apt/keyrings/docker.gpg

- name: download docker-ce repository list
  shell: |
    echo \
    "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
    "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
    tee /etc/apt/sources.list.d/docker.list > /dev/null

- name: apt update
  command: apt-get update

- name: install docker-ce and related pkg
  apt:
    name:
      - docker-ce
      - docker-ce-cli
      - containerd.io
      - docker-compose-plugin
    update_cache: yes
    state: present
# roles/kubernetes/tasks/main.yaml
- name: swap off
  command: swapoff -a

- name: apt update
  command: apt-get update

- name: install package
  apt:
    name:
      - apt-transport-https
      - ca-certificates
      - curl
      - gnupg
    update_cache: yes
    state: present

- name: download ubuntu official gpg key
  shell: curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

- name: downwload kubernetes repository list
  shell: echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

- name: apt update
  command: apt-get update

- name: install kubernetes
  apt:
    name:
      - kubelet
      - kubeadm
      - kubectl
    update_cache: yes
    state: present

- name: hold kubernetes version
  command: apt-mark hold kubelet kubeadm kubectl

yaml以外のファイル(~/ansible/roles/containerd/files に格納したファイル群)は、https://blog.estampie.work/archives/2585 の containerdの設定 でヒアドキュメント使って作成したりデフォルトの設定ファイルを書き換えて配置したりしているのを格納している。
ansible使っている割に冪等性を投げ捨てているのは気にしてはいけない

コマンド実行でEC2インスタンスをお手軽に作成

必要なファイルが準備できたら、kubernetes.tfを配置したディレクトリにcdした上で terraform applyする。
すると、https://blog.estampie.work/archives/2585  の マスターノードの作成 の手前まで完了したインスタンスが出来上がる。

やってみよう part2

Podのログが見れない問題を解消

worker node構築時、10-kubeadm.conf で指定するIPをプライベートIPにすることで無事Podのログが見えるようになった。

NodePortでServiceを作ってアクセス確認

https://cstoku.dev/posts/2018/k8sdojo-09/  を参考にDeploymentとNodePortのServiceを作成し、worker用インスタンスのパブリックDNS:作成されたNodePortのポート番号をブラウザに入力すると以下画像のようにNginxのページが開くことが確認できた。

今後の展望

misskey鯖公開を見据えてingressを導入したり、misskeyをセットアップしてみたりまでやり切りたいお気持ち。

この記事が気に入ったらサポートをしてみませんか?