Article:

IaCいいですよね。

なのでこの記事ではTerraform-provider-esxiを使って、おうちESXIをTerraformで管理してVMを立ち上げ、cloud-initを活用してTeleportを自動セットアップします。 子ノードを作成して接続まで一応するのですが、Teleportはノードの接続にinviteトークンを発行する必要があり、そこだけ自動化できていません。(今後の課題)

Terraformについては過去の記事GithubActions + TerraformでAWS開発を、TeleportについてはDockerでお手軽にTeleportを試すも参考にしてください。

terraform-provider-esxi

terraformはproviderと呼ばれるいわゆるプラグインを用いて、様々なサービス等と接続できます。 今回はterraform-provider-esxiという有志の方が作ってくれているproviderを使います。 ミニマムにまとまっていて使いやすいのですが、安定性はちょっと微妙かもしれないです。趣味なら全然使えるという感じですね。

hashicorp社が公式に出しているvsphere-providerというのもあって、vsphere越しにESXIを使えて良さげなのですがそもそもvsphereはすごいお値段がするので個人で所有するのはちょっと…という感じですね。 このproviderで、vsphere抜きにしてESXIだけ触れたら超最高なのですが、そう上手くはいかないですね。

この記事ではコードの断片的な部分しか載せていないので最終形はGithubのレポジトリを参照してください。

前準備

ESXIがインストールされたマシンはもちろんとして、ホストsshが有効になっている必要があります。

とりあえずVMを起動

自分でosをインストールするのは大変すぎるのでOVAファイルをもとにセットアップします。

ubuntuのOVAファイルはUbuntu Cloud Imagesから手に入ります。ダウンロードしてmain.tfと同じディレクトリに入れておいてください。

main.tfファイルと、tfvarsファイルを作ります(一応)。

main.tf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
variable "esxi_host"     {}
variable "esxi_user"     {}
variable "esxi_password" {}

terraform {
	required_providers {
		esxi = {
			source = "josenk/esxi"
			version = "1.10.2"
		}
	}
}

provider "esxi" {
	esxi_hostname = var.esxi_host
	esxi_username = var.esxi_user
	esxi_password = var.esxi_password
}

resource "esxi_guest" "teleport_proxy" {

	guest_name     = "teleport_proxy"
	disk_store     = "Data" # お使いの環境に合わせてください
	virthwver      = "11"   # お使いの環境に合わせてください
	power          = "on"
	boot_disk_size = 32
	guestos        = "ubuntu"
	ovf_source     = "./bionic-server-cloudimg-amd64.ova"

	network_interfaces {
		virtual_network = "playground" # お使いの環境に合わせてください
	}

}
vars.tfvars
1
2
3
esxi_host     = "*.*.*.*"
esxi_user     = "*******"
esxi_password = "*******"

特に私がハマったのは、virthwver (仮想マシンのハードウェアバージョン)のデフォルトが、自分が使っているESXIが対応していないバージョンだった為に 変なところでterraformのapplyが失敗してしまって、tfstateとの動機が切れてしまうという問題です。きっちり指定してあげましょう。

できたら、初回のterraform initをして、その後plan, applyと進みます。

また、plan, applyでは作成したtfvarsを、--var-file vars.tfvarsと指定してあげます。

applyしてもりもり処理が走って、

1
apply complete! Resources: 1 added, 0 changed, 0 destroyed.

と表示されれば成功です!

cloud-initでユーザー作成

前章で無事VMが作成できたのはいいのですが、まずsshはできませんし、esxiのclientのコンソールでもログインプロンプトはでてるものの何を入れたらいいかわからないかと思います。だって設定してないですし。

というわけでcloud-initを使って設定していきます。

cloud-initはyaml形式でサーバーの初期設定をする仕組みです。

ユーザーとか、インストールパッケージとか、実行するコマンドとか、様々な実行項目が「モジュール」として定義されていて、モジュール単位で設定をぐるぐる練っていく感じです。 モジュール一覧は公式ドキュメントを見てください。

まずは設定を読み込む部分をtfファイルに書きます

main.tf(抜粋)
1
2
3
4
5
6
7
8
9
resource "esxi_guest" "teleport_proxy" {

	...

	guestinfo = {
		"userdata"          = base64gzip(file("./cloud-init.yaml"))
		"userdata.encoding" = "gzip+base64"
	}
}

terraformではfile関数で外部のファイルの文字列を取り込めます。便利。

では本体のcloud-init.yamlを作ります。

cloud-init.yaml
1
2
3
4
5
6
7
8
9
#cloud-config
users:
    - name: testuser
      plain_text_passwd: "password"
      lock_passwd: false
      sudo: ALL=(ALL) NOPASSWD:ALL
      shell: /bin/bash
      ssh_authorized_keys:
          - ssh-rsa AAAAAAAAAAAAAAA= MYHOSTNAME

先頭の#cloud-configが超重要です!!!

cloud-initはシェルスクリプトかcloud-init形式か選べて、先頭にこのコメントがあれば後者になるという仕組みです。 これをtypoしてるとうまくいきません。気をつけてください。

あと、大変申し訳無いのですが以後でてくるcloud-init.yamlは全部抜粋なのでこの行を省きます。この説明を見落とされないことを願います。

lock_passwdのデフォルト値はtrueなので、デフォルトだとパスワードログインができないところが注意ですね。

これで再度terraform applyすれば自動的にVMが 再生成 されます。((以前のは強制的に消されちゃいます) これでターミナルでパスワードログインしたり、sshで接続できたりするはずです(IPは頑張って調べてください)。

IP固定

サーバーとして使うならIPを固定したいですよね。 ネットワーク関連の設定は公式ドキュメントのここになります。

でもって、これらの設定はuserdataとは別枠で設定することになります。さっきcloud-init.yamlを追加したノリでmeta.yamlを追加します。

main.tf(抜粋)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
resource "esxi_guest" "teleport_proxy" {

	...

	guestinfo = {
		"userdata"          = base64gzip(file("./cloud-init.yaml"))
		"userdata.encoding" = "gzip+base64"
		"metadata"          = base64gzip(file("./meta.yaml"))
		"metadata.encoding" = "gzip+base64"
	}
}
meta.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
network:
    version: 1
    config:
        - type: physical
          name: ens32
          subnets:
              - type: static
                address: 0.0.0.0/0 # 設定したい固定IP
                gateway: 0.0.0.0 # おうちのGW
        - type: nameserver
          address:
              - 0.0.0.0 # おうちのDNS

でもって例によってterraform applyすれば再生成されて指定したIPで新しく生まれてくるはずです。

teleportをセットアップ

ではteleportをインストールしていきましてよ。

基本的にパッケージをインストールするのは、packageモジュールにパッケージ名を列挙するだけなんですけど、teleportは独自のレポジトリを追加する必要があるのでひと手間かかります。

順を追って説明します。

レポジトリを追加

keyidにfigerprintを入力するところ以外は普通ですね

1
2
3
4
5
6
apt:
    sources:
        teleport:
            keyserver: "https://deb.releases.teleport.dev/teleport-pubkey.asc"
            keyid: "0c5e 8ba5 658e 320d 1b03 1179 c87e d53a 6282 c411"
            source: "deb [signed-by=$KEY_FILE] https://deb.releases.teleport.dev/ stable main"

パッケージのインストール

他にもインストールしたいパッケージがあればここに追加できます。

1
2
3
4
package_update: true
package_upgrade: true
packages:
    - teleport

自動起動の設定

自動起動を設定して初回起動も入れます。

1
2
3
runcmd:
    - [ sudo, systemctl, enable, teleport.service ]
    - [ sudo, systemctl, start, teleport.service ]

類似モジュールにbootcmdというのもありますが、runcmdがセットアップ時に1度だけ、処理の最後の方で実行されるのに対し、bootcmdは処理の最初の方に、毎起動時実行されるコマンドになります。

上記3つの設定を加えてterraform applyしたらteleportが立ち上がってるはずです。

configファイルの流し込み

teleport用の設定ファイルをサーバーに入れたいですよね。 ただyamlで外部ファイルをincludeできないのでyamlに直接埋め込む形にはなります… これはなんとかしたいですね。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
write_files:
    - path: /etc/teleport.yaml
      content: |
        version: v2
        teleport:
          nodename: 10.58.0.101
          #data_dir: /persistent/teleport #これは後で使う
          log:
            output: stderr
            severity: INFO
            format:
              output: text
          ca_pin: []
          diag_addr: ""
        auth_service:
          enabled: "yes"
          listen_addr: 0.0.0.0:3025
          proxy_listener_mode: multiplex
        ssh_service:
          enabled: "yes"
          labels:
            level: "restricted"
          commands:
          - name: hostname
            command: [hostname]
            period: 1m0s
        proxy_service:
          enabled: "yes"
          https_keypairs: []
          acme: {}        

これで/etc/teleport.yamlにファイルを作成できます。

恒久ストレージを追加

今までの方法でteleportをセットアップできますが、いかんせんimmutable infrastructureって感じで設定を変更するとサーバーが再作成されちゃうので、永続性がないですよね(それが狙いではあるのですが)。

ここで、OSの入っているメインストレージの他にサブのストレージを入れて、永続化したい情報はそっちを向くようにします。

まずはmain.tfでストレージリソースを作成し、vmに接続します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
resource "esxi_virtual_disk" "proxy_disk" {
	virtual_disk_disk_store = "Data" # ご自身のHDD名を入力
	virtual_disk_dir        = "Terraform"
	virtual_disk_name       = "proxy_disk.vdmk" # 名前の最後が.vdmkなのは必須
	virtual_disk_size       = 32
}

resource "esxi_guest" "teleport_proxy" {

	...

	virtual_disks {
		virtual_disk_id = esxi_virtual_disk.proxy_disk.id
		slot            = "0:1"
	}
}

でもって、cloud-initでディスクの認識・フォーマット・マウントを行います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
device_aliases:
    persistent: /dev/sdb

disk_setup:
    persistent:
        table_type: gpt
        layout: [100]

fs_setup:
    - label: fs1
      filesystem: ext4
      device: persistent.1

mounts:
    - ["persistent.1", "/persistent"]

でもってteleportのコンフィグファイルのdatadirの項目が/persistentを向くようにすれば、何度破壊されても同じ認証情報でログインできるはずです。

この状態で、tctl tokens add –type=nodeして、inviteトークンを生成してメモっておきます。(ついでにca-pinも)

子ノードとの接続(問題あり)

今までと同じ要領でリソース・cloud-initのファイルを作成してteleportの子ノードを作成します。 teleportの設定にauth_servers, auth_token, ca_pinを追記すれば自動的にクラスターに参加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
write_files:
    - path: /etc/teleport.yaml
      content: |
        version: v2
        teleport:
          nodename: *.*.*.*
          data_dir: /persistent/teleport
          auth_token: "********************************"
          ca_pin: "sha256:****************************************************************"
          auth_servers:
              - *.*.*.*:3025
          log:
            output: stderr
            severity: INFO
            format:
              output: text
          diag_addr: ""
        auth_service:
          enabled: "no"
        ssh_service:
          enabled: "yes"
          labels:
            level: "normal"
          commands:
          - name: hostname
            command: [hostname]
            period: 1m0s
        proxy_service:
          enabled: "no"        

ただinviteトークンの発行は手動でしたし、トークンにも有効期限があるのであんまりイカした方法とは呼べないですよね…

たぶんteleportにこの辺を柔軟にするAPIがないので、自分でコマンドを自動で叩いてくれるシステムを追加するか…という感じです。

記事の最初にも書きましたが見逃しちゃった人のためにもう一度… この記事ではコードの断片的な部分しか載せていないので最終形はGithubのレポジトリを参照してください。

今後

cloud-initはシンプルにまとまってて便利ではあるものの、これでサーバー全部構築できるかというとだいぶ無理がありますよね…普通にchefとか使いたい感じです。

cloud-initにはchefを呼び出す機能自体はあるのですが、ていうかchefはそもそも有料になってしまったのでCINCを使いたいですし、CINC Serverを用意して管理して…というのもちょっとめんどくさいです。

もうちょっと良げなプロビジョニング手法を探そうと思います。