Terraform + Cloud-init + Ansible で IBM Cloud VSIプロビジョニング自動化

米国では Terraform のスキルを持っていると、Kubernetesに次いで、転職に有利なんだそうで、人気のツールだそうです[1]。IBM Cloud のドキュメントでも Terraform を利用したものがたいへん増えており[2,3,4]、ツールとしての有用性の高さが伺えます。


このメモでは、次の３つのツールを利用して、IBM クラウドのプロビジョニングと設定を自動化します。この中で、IBM Cloud に固有な設定含むのは、Terraform の設定ファイルだけですから、ここだけを入れ替えれば、他のクラウドでも利用できるようになります。


* Terraform  クラウドの仮想サーバーのプロビジョニング
* Cloud-init 仮想サーバー初回起動時のコンフィグレーション
* Ansible    仮想サーバーの自動設定

これらのツールについて、自分の頭の整理もかねて、簡単に説明を書いておきます。


### Terraform とは

Terraformは、HashiCorp社 によって開発された、インフラを構築、変更、およびバージョン管理するためのツールです。Terraformは、AWSなどのクラウド・サービスプロバイダやオーケストレータを管理できます。アプリの基盤となる仮想サーバーのコンポーネントをTerraformの構成ファイルに記述します。この構成ファイルに書かれた目的の状態に到達するために、必要な実行計画を生成しインフラを構築します。また、構成ファイルが変更されると、Terraformは、変更箇所を判別し、増分実行プランを作成して、目的の状態へ移行していきます。
Terraformが、管理できるインフラには、コンピューティングインスタンス、ストレージ、ネットワークなどの低レベルのコンポーネント、およびDNSエントリ、SaaSなどの高レベルのコンポーネントも含まれます。[5]

HashiCorp社が開発したソフトウェアの　Vagrant は、ソフトウェア開発者の間で、たいへん人気であり、筆者も本当にお世話になっています。これは、自身の開発用ワークステーションの上で、複数の仮想サーバーを起動して、お互いに連携させて利用できる機能をもった、ソフトウェアであり、Vagrantと Terrraform の２系統が存在する理由が、最初は解りませんでした。

HashiCorp社の説明 [6] によると、Vagrantは開発環境用で、Terraformは、汎用的なインフラストラクチャ管理用とのことです。Terraformの主な用途は、AWSなどのクラウドプロバイダーでリモートリソースを管理することです。Terraformは、複数のクラウドプロバイダーにわたる非常に大規模なインフラストラクチャを管理できるように設計されています。Vagrantは主に、少数の仮想サーバーを用いるローカル開発環境向けに設計されています。


### Cloud-init　とは

Cloud-initは、AWS EC2 の LinuxのUbuntuディストリビューション用に、Canonical社が開発しましたが、現在は、すべての主要クラウドのLinuxディストリビューションでサポートされています。[7]

* Ubuntu
* archlinux
* CentOS
* RedHat
* FreeBSD
* Fedora
* gentoo linux
* SUSE Linux

Cloud-initの役割は、クラウドのテンプレートイメージから、インスタンスの初回起動時に、ホスト名、ローケール、マウントポイントなど、ユーザーの指定に従って自動的に設定するツールです。Terraform から　クラウドのインスタンスを起動する際に、Cloud-init へ渡すための YAMLファイルを指定することができ、パッケージの追加、コマンドの実行、ファイルの作成などができます[8]。

Ansible や Chef などのサーバー自動設定ツールは、目的とする状態を変更して、何度でも適用できます。しかし、Cloud-initは、初回起動時だけしか実行されません。そのため、本メモでは、このツールを利用して、Python と Ansible をバージョン指定でインストールすることに、利用しています。


### Ansible とは

Ansibleを改めて説明することは、ほとんど必要ないくらい、有名なツールですね。現在はRedHat社がメンテナンスを担当しており、Pythonで書かれたエージェントレスなサーバー自動設定ツールで、ssh を使ってリモートから設定を実行します。 構成ファイルは、プレイブックと呼ばれ、非常に多くのライブラリが充実しています。[9,10]

今回は、Terraform と組み合わせて、既存のAnsible資産を活かしつつ、Terraform でマルチクラウドを利用することを考えていきます。

---

## Terraform 実行のワークステーション環境

Terraform を操作するワークステーションは、Vagrant で起動する仮想サーバーを利用することにします。そして、Vagrantの構成ファイルからAnsibleのプレイブックを呼出して、セットアップを完了させます。

この理由は、パソコンに、TerraformやAnsibleをインストールするより、Vagrantで起動するパソコン上の仮想サーバーに、ツールをインストールして利用することで、パソコンにインストールするソフトのを減らし、それらによって安定性が損なわれることを回避することができます。また、Vagrant と　Ansible のファイルに Terraform のセットアップ自動設定を書いておけば、何度でも再利用できますし、チームメンバーと共有することもできます。

この仮想サーバーを起動するための Vagrantfileは、GitHub [takara9/vagrant-terraform](https://github.com/takara9/vagrant-terraform) にあります。これをクローン(git clone)してアップ(vagrant up)するだけで、Terraform と　Terraform　の　IBM Cloud プロバイダー、Ansible 、IBM Cloud CLI がインストールした状態になります。 このVagrant で起動するパソコン上の仮想マシンから、IBMクラウドの仮想サーバーを操作することにします。


## Terraform でプロビジョニングして、Ansibleで自動設定するまで

Terraformを使って、IBM Cloud の仮想サーバーをプロビジョニングして、Ansile で自動設定するまでの流れです。この操作で １台から10台、さらに100台規模の仮想サーバーを起動することができます。クラウドのコンソールにアクセスすることなく、操作するパソコンに個別ソフトをインストールすることなく、クラウドの処理パワーを利用する準備が整います。

この実施の流れには、以下を見て解るように、３つの段階があります。それぞれの段階について、その概要を書いていきます。コードの詳細や実行のための認証情報の取得とセット方法は、それぞれの GitHub の README.md 参照してください。

~~~
## パソコン上のワークステーションを開始
$ git clone https://github.com/takara9/vagrant-terraform
$ cd vagrant-terraform
$ vagrant up
$ vagrant ssh
## Terraform の構成ファイルを取得、プロビジョニング実行
$ git clone https://github.com/takara9/terraform-ibmcloud-vsi
$ cd terraform-ibmcloud-vsi
$ terraform init -plugin-dir /usr/local/bin
$ terraform plan -var-file /vagrant/.secret.tfvars
$ terraform apply -var-file /vagrant/.secret.tfvars
## Ansibleのプレイブックを適用
$ python creat_inventry_ansible_f_terraform.py
$ ansible -m ping -i playbooks/hosts nodes
$ ansible-playbook -i playbooks/hosts playbooks/setup.yml
~~~

### パソコン上のワークステーションを開始

Ansible や Terraform などのソフトウェアを、パソコンにインストールする時間を省くために、Vagrant と　Ansible を使って自動化します。この vagrant up によって、以下のソフトウェアがインストールされた　Ubuntu の仮想サーバーが起動します。　これを利用して、Terraform を実行します。

~~~
## パソコン上のワークステーションを開始
$ git clone https://github.com/takara9/vagrant-terraform
$ cd vagrant-terraform
$ vagrant up
$ vagrant ssh
~~~

この Vagrantfile などから、インストールされるパッケージは以下になります。このように様々なツールを備えて、様々な作業を実施できるようにするため、筆者はワークステーションと呼ぶことにしました。

* ansible
* python-minimal, python-pip
* unzip
* terraform
* terraform-provider for IBM Cloud
* IBM Cloud の仮想サーバーの認証鍵
* IBM Cloud CLI (Git,Docker,Helm,kubectl,curl,IBM Cloud Developer Tools plugin, IBM Cloud Functions plugin, IBM Cloud Container Registry plugin, IBM Cloud Kubernetes Service plugin, sdk-gen plugin が含まれます)

それぞれのウェブページを参照しながら、手作業でインストールすると、軽く数時間かかってしまうと思います。この様な作業は、どんどん自動化して、新しいことに取り組むことに、時間を使えるようにしたいものです。

「IBM Cloud の仮想サーバーの認証鍵」は、パソコンのターミナルソフトからログインする、または、Terraform　や　Ansible からリモート操作するために、必要な ssh の認証鍵になります。　これは事前に、ssh鍵のペアを生成して、クラウドに登録しておき、プロビジョニング時に自動的に埋め込むように、事前準備が必要です。 具体的な操作は、この GitHub リポジトリの　keys/README.md を参照してください。


### Terraform の構成ファイルを取得、プロビジョニング実行

Terraform では、それぞれのクラウドベンダーに対応するために、Terraform-Provider と呼ばれるプラグインを利用します。 AWS や Azure などメジャーなクラウドでは、構成ファイルに書かれたプロバイダー名から判断して、必要なプラグインをダウンロードしてくれます。しかし、IBM Cloudの場合は、HashiCorp に登録されていませんから、terraform init 実行時にプラグインを指定しなければなりません。このプラグインは、ワークステーションの構成時に、自動的にダウンロードして、/usr/local/bin に配置してあります。

terraform のコマンド操作は、その構成ファイルが存在するディレクトリで実行しなければなりません。そこで、git clone したディレクトリへ移動して、操作を進めます。

~~~
## Terraform の構成ファイルを取得、プロビジョニング実行
$ git clone https://github.com/takara9/terraform-ibmcloud-vsi
$ cd terraform-ibmcloud-vsi
$ terraform init -plugin-dir /usr/local/bin
$ terraform plan -var-file /vagrant/.secret.tfvars
$ terraform apply -var-file /vagrant/.secret.tfvars
~~~

次に、ドライラン (dry-run) を実行して構成ファイルに問題がないかを確認します。　ドライランとは、実際に変更を加えない実行を表し、Terraformでは、サブコマンド「plan」で実施できます。　
オプション「-var-file /vagrant/.secret.tfvars」は、Terraform の変数だけを抜き出したファイルです。GitHubなどで公開できない情報、すなわち、APIキーやssh鍵などを、このファイルに記述しておき、セキュリティを確保します。

次は、このディレクトリの Terraform に関係するファイルだけを抜き出して、役割りをコメントしたものです。

~~~
$ tree terraform-ibmcloud-vsi/
terraform-ibmcloud-vsi/
├── install.yml　　　   // Cloud-init の構成ファイル (main.tfで利用される)
├── main.tf            // Terraform のメインの構成ファイル
├── terraform.tfstate  // Terraform 実行後に状態を出力したファイル（apply実行後に生成）
└── terraform.tfvars   // Terraform の変数ファイル
~~~

main.tf に、次の HasiCorp の記述言語[11]の文法に従って、IBM Cloud プロバイダー[12]のコマンドを記述します。　

* [HashiCorp Configuration Language (HCL)](https://github.com/hashicorp/hcl)
* [IBM Cloud Provider for Terraform](https://ibm-cloud.github.io/tf-ibm-docs/v0.14.1/)


main.tf に記述する内容は、利用するプロバイダーによって、異なります。 次は、main.tf の一部のプロバイダーを指定する部分です。

~~~
provider "ibm" {
  softlayer_username = "${var.sl_username}"
  softlayer_api_key  = "${var.sl_api_key}"
  bluemix_api_key    = "${var.ic_api_key}"
}
~~~

このなかで、softlayer_username の変数の役割や意味は、[IBM Cloud Provider for Terraform](https://ibm-cloud.github.io/tf-ibm-docs/v0.14.1/)　に記述されています。そして、この変数の値は、コマンドのオプションとして指定した　/vagrant/.secret.tfvars に記述しておきます。

そして、クラウドベンダー固有の設定は、｢resource "ibm_compute_vm_instance" "vsi" {｣　に書いていきます。　この設定項目も [IBM Cloud Provider for Terraform](https://ibm-cloud.github.io/tf-ibm-docs/v0.14.1/) の項目に従って記述していきます。

~~~
# https://ibm-cloud.github.io/tf-ibm-docs/v0.14.1/r/compute_vm_instance.html
#
resource "ibm_compute_vm_instance" "vsi" {
  count                    = "${var.num_of_vsi}"
  hostname                 = "${var.hostname}-${count.index}"
  os_reference_code        = "${var.os_code}"
  domain                   = "${var.domain}"
  datacenter               = "${var.datacenter}"
  network_speed            = "${var.nic_speed}"
  hourly_billing           = true
  private_network_only     = "${var.private_only}"
  cores                    = "${var.vcpu}"
  memory                   = "${var.ram}"
  ＜省略＞
  user_metadata            = "${file("install.yml")}"
  ＜以下省略＞
~~~

この中で、「count」は、特殊な項目ですから、取り上げておきます。　例えば、この値を 10 に設定すると、同一スペックの仮想サーバーが 10台 プロビジョニングされます。　ループを回したりする必要はありません。　それぞれのホスト名は、"${var.hostname}-${count.index}"　とすることで、ベースのホスト名の末尾に数字をつけることができます。

「user_metadata」の指定によって、Cloud-init に渡すための YAMLファイルが設定します。　メジャーなクラウドプロバイダーのLinuxディストリビューションの仮想サーバーには、Cloud-initが導入されていますから、CentOS / RedHat などにも適用できます。　

このYAMLファイルには、 必要最小限のパッケージをインストールするように設定[8]して、本格的な自動設定はAnsibleで行います。ここでは、Linux ディストリビューションにAnsibleのバージョンが依存しないように、Python のパッケージマネージャー pip から　Ansible のバージョンを指定してインストールします。

~~~
#cloud-config
package_update: true
packages:
  - git
  - python-pip
runcmd:
  - pip install ansible\==2.7.7
~~~


### Ansibleのプレイブックを適用

Terraformを使って、クラウドの仮想サーバーを起動して、Ansible がインストールされましたから、あとは、プレイブックを適用するだけですが、そのまえに、Ansible のインベントリーファイルを作成して、対象の仮想サーバーのホスト名、IPアドレス、認証鍵を指定する必要があります。しかし、現在、残念ながら、Ansibleのプロバイダーが存在しません。　Terraform-inventory [13] という Ansible のオプションを指定して、利用できるコードもありますが、あまり進んでいないようです。

そこで、Terraform　の実行時に出力される terraform.tfstate のJSON形式の情報から、outputs の内容を読み取って、Ansible のインベントリーファイルを作成する Pythonのコード　creat_inventry_ansible_f_terraform.py　を書きました。　このコードからの出力は、playbooks/hosts に書き込みます。このコードの詳細は、GitHubリポジトリを参照してください。　コメントを付与してありますから、簡単に読めると思います。

~~~
## Ansibleのプレイブックを適用
$ python creat_inventry_ansible_f_terraform.py
$ ansible -m ping -i playbooks/hosts nodes
$ ansible-playbook -i playbooks/hosts playbooks/setup.yml
~~~

Terraform　からプロビジョニングされた仮想サーバーのホスト名とIPアドレスを含むインベントリーファイルが出来たので、ansible -m ping を実行して、疎通を確認します。　そして、プレイブックを適用します。ここでは、主題がAnsibleのプレイブックを書くことではないので、プレイブックでホスト名を表示して、連携を確認するまでにとどめます。

~~~
vagrant@workstation:~/terraform-ibmcloud-vsi$ ansible -m ping -i playbooks/hosts all
workstation | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
worker-0 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
worker-2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
worker-1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
vagrant@workstation:~/terraform-ibmcloud-vsi$ ansible-playbook -i playbooks/hosts playbooks/setup.yml

PLAY [nodes] ****************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************
ok: [worker-1]
ok: [worker-0]
ok: [worker-2]

TASK [Test] *****************************************************************************************************
changed: [worker-0]
changed: [worker-2]
changed: [worker-1]

TASK [Debug] ****************************************************************************************************
ok: [worker-0] => {
    "result.stdout": "worker-0"
}
ok: [worker-1] => {
    "result.stdout": "worker-1"
}
ok: [worker-2] => {
    "result.stdout": "worker-2"
}

PLAY RECAP ******************************************************************************************************
worker-0                   : ok=3    changed=1    unreachable=0    failed=0
worker-1                   : ok=3    changed=1    unreachable=0    failed=0
worker-2                   : ok=3    changed=1    unreachable=0    failed=0
~~~

## クリーンナップ

最後にTerraform でデプロイした仮想サーバーを消す方法は、apply の変わりに、destroy を指定します。

~~~
terraform destroy -var-file /vagrant/.secret.tfvars
~~~


## まとめ

クラウドは、様々な OSS を集大成するソリューションであり、有用な OSS を組み合わせて、目的を素早く達成する手段として利用できます。 もちろん、個々のOSSを適切に理解して、長所を組み合わせることができなければなりません。　また、それぞれを機能を補完して繋ぎ合わせるためのコードを書くスキルも大切なんだと思います。

今回、学んだ Terraform, Cloud-init, Ansible は、クラウドを活用したソリューションを開発する者にとって、大切なスキルではないかと思います。

IBM Cloud ドキュメントドキュメントを見ていたら、似たようなことを書いた記事[14]がありました（笑）コード書いちゃった後で見ると...


## 参考資料
[1] The Top Tech Skills of 2018: Kotlin & Kubernetes Made Their Mark, https://insights.dice.com/2019/01/02/top-tech-skills-2018-kotlin-kubernetes/?fbclid=IwAR3tQQ6Y_HRBGFWP8-LtkXtINPtLVYnDXd_R1YqePVTCrm1GrMXdUm1UvL4
[2] Infrastructure automation with Terraform on IBM Cloud IaaS, https://developer.ibm.com/recipes/tutorials/infrastructure-automation-with-terraform-on-ibm-cloud-iaas/
[3] Deploy a LAMP stack using Terraform, https://cloud.ibm.com/docs/tutorials/infrastructure-as-code-terraform.html#deploy-a-lamp-stack-using-terraform
[4] Infrastructure as Code: Chef, Ansible, Puppet, or Terraform?, https://www.ibm.com/blogs/bluemix/2018/11/chef-ansible-puppet-terraform/
[5] What is Terraform?, https://www.terraform.io/intro/index.html
[6] Vagrant vs. Terraform, https://www.vagrantup.com/intro/vs/terraform.html
[7] Cloud-init, https://cloud-init.io/
[8] Cloud-init Documentation, https://cloudinit.readthedocs.io/en/latest/
[9] RedHat Ansible, https://www.ansible.com/products/tower?extIdCarryOver=true&sc_cid=701f2000001OH7YAAW
[10] Ansible Documentation, https://docs.ansible.com/ansible/latest/index.html
[11] HashiCorp Configuration Language, https://github.com/hashicorp/hcl
[12] IBM Cloud Provider for Terraform, https://ibm-cloud.github.io/tf-ibm-docs/v0.14.1/
[13] adammck/terraform-inventory, https://github.com/adammck/terraform-inventory
[14] Using Ansible to automate app deployment on Terraform-provided infrastructure, https://console.bluemix.net/docs/terraform/ansible/ansible.html#ansible