puppet bolt のディレクトリ構成
この記事は Puppet Advent Calendar 2020 16 日目 の記事です。
はじめに
まず、本記事を書こうと思ったモチベーションは
- puppet bolt に関する記事が足りない!!
- あったとしても、1,2 年前の日本語の記事で、かつ、なんか最近の bolt と違う感じがする!!
- じゃあ、公式ドキュメントだ!と思っても、実際やろうと思うと、かゆいところまでは、手が届いていない感!!
- というか、puppet bolt 自体が発展途上で、そもそも色々足りない!!
- puppet bolt を使いたいけど、まず、何すればいいの?
- このツールの全体感がわからない。
です。もし私と同様に puppet bolt を触ろうと思っている方が
- 「あーはいはい。そんな感じで使えばいいのね。雰囲気わかった。」
って状態になっていただければ、幸いです。
ただし、前置きすると、上記に記載した通り、「まだまだ発展途上感がある」と感じているため、この記事を目にした時点で、だいぶ変わっている可能性もあると思っていますので、そこのところは、ご留意ください。
puppet bolt のディレクトリ構成
本記事で説明する puppet bolt のディレクトリ構成は以下の通りです。それぞれに関して、簡単にですが、解説して行きたいと思います。
$ tree -a -L 3
.
├── .modules
│ ├── chocolatey
│ └── ...
├── .resource_types
│ └── ...
├── Puppetfile
├── README.md
├── bolt-debug.log
├── bolt-project.yaml
├── data
│ ├── common.yaml
│ ├── local.yaml
│ ├── production.yaml
│ └── staging.yaml
├── hiera.yaml
├── inventory.yaml
├── site-modules
│ └── mymodule
│ ├── files
│ │ ├── motd.txt
│ │ └── check_server.sh
│ ├── plans
│ │ ├── planA.yaml
│ │ └── planB.yaml
│ └── templates
│ └── env.epp
└── tasks
├── download_from_gcs.json
└── download_from_gcs.rb
各ディレクトリ・ファイルについて
.modules
Puppet Forgeの puppet modules が install されるディレクトリです。node.js でいうと node_modules 的なディレクトリです。基本的には、ここのディレクトリは、変更しない方針であることが前提で、gitignore しても問題ありません。もし修正した場合は、.gitignore しないで、含めた方がいいと思います。ちなみにここに関しては、puppet bolt を利用する上では、知らなくても、まぁなんとかなります。
.resource_types
Puppet Forgeの puppet modules に関連する Resource Type が install されるディレクトリ。基本的には、このディレクトリは、変更しない方針であることが前提で、gitignore しても問題ない。また、puppet bolt を利用する上では、知らなくても、基本問題ありません。
「Resource Type
とはなんぞや?」というところを理解したい場合は、まずは「Resource
とはなんぞや?」というところから、調べてみることをオススメします。
ref.
Puppetfile
Puppet Forgeの puppet modules を「初めて」 install した時に、作成されるファイル。基本的には、このファイルは、手で修正しない方針であることが前提(Do not edit)。このファイルをベースに、.modules
を install するため、基本的には、gitignore はしない方がいいと個人的には思っています。ファイル名的には、Gemfile
っぽいファイルと思いきや、Gemfile.lock
のようなファイル。
# This Puppetfile is managed by Bolt. Do not edit.
# For more information, see https://pup.pt/bolt-modules
# The following directive installs modules to the managed moduledir.
moduledir '.modules'
mod "puppetlabs-chocolatey", "5.1.1"
・・・
bolt-debug.log
Bolt コマンドを実行時、デバッグレベルのログが出力されるログファイルです。bolt plan
などの実行ログが書き込まれます。ログの形式は以下の通りです。特に何も指定しなければ、実行毎に recreate されるため、過去のログは残らなさそうです。基本的には、.gitignore
などで、git からは除外することが多いと思います。
2020-12-02T18:44:46.273566 INFO [event-publisher] [Bolt::Outputter::Logger] Starting: plan mymodule::planA
2020-12-02T18:44:46.291736 INFO [event-publisher] [Bolt::Outputter::Logger] Finished: plan mymodule::planA in 0.02 sec
bolt-project.yaml
project 単位(リポジトリ単位?)で、bolt の設定を管理する時に用意するファイル。ansible でいうところの ansible.cfg みたいなもの。今までは、bolt.yaml
を使っていたみたいですが、そちらは非推奨になっています。
# bolt project の作成( project_name に、ハイフンは含められない。)
$ bolt project init myproject
$ ls
bolt-project.yaml
$ cat bolt-project.yaml
---
name: myproject
ref. bolt-project.yaml options
data
bolt で利用する変数などを管理するディレクトリ。一般的には、OS や環境毎や構成管理ツール毎などの yaml ファイルを格納しておくことが多いです。後述する hiera の機能で、その変数等を読み込む優先順位を定義します。格納できるファイルは、YAML or JSON or HOCON(Human-Optimized Config Object Notation)が利用できます。ディレクトリ名は data
である必要はありませんが、大体、 data
とすることが多いようです。簡単な yaml の例を記載するとすると以下のような形で、普通の yaml として定義するだけです。
# staging.yaml
---
gcp_project_id: "gcp_project_staging"
# production.yaml
---
gcp_project_id: "gcp_project_production"
ref. Creating and editing data
hiera.yaml
data
ディレクトリに格納した変数ファイルの読み込む優先順位などを定義するファイル。 hierarchy
に、ハッシュの配列形式で読み込む順番(優先順位)を定義する。上から下に、優先順位が下がっていきます。そのため基本的には、最後に、common.yaml
とかを記載することが多いです。ansible だと、group_vars
とか host_vars
とか暗黙的な変数定義ルールがある印象ですが、多分?、puppet bolt の変数定義ルールはここに全て記載する必要があるような気がしています。
---
version: 5 # hiera の ver 指定する。bolt は ver 5のみ対応している。
defaults:
datadir: data # 変数ファイルを、hiera.yaml ファイルからみた相対パスで指定する
data_hash: yaml_data # 変数ファイルの形式を指定する。e.g. yaml, json, HOCON
hierarchy:
# bolt 実行時に、env を渡すとそれぞれの変数ファイルを読み込む
# 上から優先順位が高い。そのため基本的には、最後に、common.yamlとかを記載することが多い。
- name: "environment data"
path: "%{env}.yaml"
- name: "common data"
path: "common.yaml"
上記 hiera.yaml を定義した場合、下記のように bolt plan 実行時に、引数にenv
実行すると、各環境の変数ファイルを読み込むことが可能です。
# 引数 env=staging -> data/staging.yaml を読み込む
$ bolt plan run mymodule::planA env=staging --targets webservers
# 引数 env=production -> data/production.yaml を読み込む
$ bolt plan run mymodule::planA env=production --targets webservers
ref.
inventory.yaml
対象ホストを管理するファイルです。ansbible の inventory ファイルと同様の役割をするファイルです。Inventory filesにある、一部の例を下記に転記しました。
groups:
# e.g. linux の webserver via ssh
- name: ssh_nodes
groups:
- name: webservers
targets:
- 192.168.100.179
- 192.168.100.180
- name: memcached
targets:
- 192.168.101.50
config:
ssh:
user: root
config:
transport: ssh
ssh:
user: centos
private-key: ~/.ssh/id_rsa
host-key-check: false
# e.g. windows の webservers via winrm
- name: win_nodes
groups:
- name: apiservers
targets:
- 192.168.110.10
- name: testservers
targets:
- 172.16.219.20
config:
transport: winrm
winrm:
user: DOMAIN\opsaccount
password: S3cretP@ssword
ssl: true
ここの例では、まず、接続方式が、「ssh」か「winrm」かで group を分けています。その後、それぞれで、複数対象ホストが存在する場合は、さらに、groups で入れ子にして、対象ホストを指定します。config には、接続方式とその認証情報を記載します。そして、bolt コマンド実行時の --targets
の引数として、groups を指定するように、下記のようなコマンドを実行します。
# ssh_nodes,win_nodes 両方に実行する場合
$ bolt command run 'echo hello' --targets ssh_nodes,win_nodes
# ssh_nodes のみに実行する場合
$ bolt command run 'echo hello' --targets ssh_nodes
# webservers,testservers のみに実行する場合
$ bolt command run 'echo hello' --targets webservers,testservers
そうすると、inventory から指定された targets への接続方法・情報を判断して、それぞれに対して、接続し、コマンドを実行します。ただし、この指定の仕方の場合、targets
に対して、IP アドレスなどを指定しなければならないため、対象ホストが増えてくると管理が煩雑になっていきます。もし設計段階で、そのような状況になりそうであれば、AWS や GCP などのクラウドを使っているのであれば、いわゆる Dynamic Inventory のような plugin 機能を利用するのは簡単にできそうなため、試してみるのが良いかなと思います。
# ref. https://forge.puppet.com/modules/puppetlabs/aws_inventory
# inventory.yaml
---
groups:
- name: aws
targets:
- _plugin: aws_inventory
profile: user1
region: us-west-1
filters:
- name: tag:Owner
values: [Devs]
- name: instance-type
values: [t2.micro, c5.large]
target_mapping:
name: public_dns_name
uri: public_ip_address
config:
ssh:
host: public_dns_name
config:
ssh:
user: ec2-user
private-key: ~/.aws/private-key.pem
host-key-check: false
ref. Inventory files
tasks
task は、対象ホストで実行する puppet bolt の中で最もシンプルなアクションの単位です。タスクは、Bash、Python、Ruby など、対象ホスト似て、実行できる任意のプログラミング言語でタスクを記述できます。デフォルトでは、下記のようなタスクが用意されています。
$ bolt task show
apt Allows you to perform apt functions
facts Gather system facts
http_request Make a HTTP or HTTPS request.
package Manage and inspect the state of packages
pkcs7::secret_createkeys Create a key pair
pkcs7::secret_decrypt Encrypt sensitive data with pkcs7
pkcs7::secret_encrypt Encrypt sensitive data with pkcs7
puppet_agent::install Install the Puppet agent package
puppet_agent::version Get the version of the Puppet agent package installed. Returns nothing if none present.
puppet_conf Inspect puppet agent configuration settings
reboot Reboots a machine
reboot::last_boot_time Gets the last boot time of a Linux or Windows system
service Manage and inspect the state of services
terraform::apply Apply an HCL manifest
terraform::destroy Destroy resources managed with Terraform
terraform::initialize Initialize a Terraform project directory
terraform::output JSON representation of Terraform outputs
例えば、package のタスクの使い方としては、以下のコマンドを実行することで、対象ホスト(webservers)の Apache の status を確認することができます。
bolt task run package action=status name=apache2 --targets webservers
カスタムタスクの例として、ruby で記載した GCS からファイルをダウンロードするタスクを紹介します。
・・・
└── tasks
├── download_from_gcs.json # タスクのメタデータ
└── download_from_gcs.rb # タスクを定義する実行スクリプト
ファイルとしては、「タスクのメタデータ」の json と「タスクを定義する実行スクリプト」を準備します。そして、それぞれのファイルを以下のように記述します。
// download_from_gcs.json
{
"description": "Download file from google cloud storage locally", // タスクの説明
"input_method": "stdin", // タスクのインプット方法 e.g. environment(環境変数), stdin(標準入力), powershell(??)
"parameters": {
"bucket_name": {
"description": "Bucket of google cloud storage", // パラメータの説明
"type": "String[1]" // パラメータのデータ型の指定 e.g. 空ではない文字列
},
"object_path": {
"description": "Object path of google cloud storage",
"type": "String[1]"
},
"tmp_dir": {
"description": "tmp path",
"type": "String[1]"
}
},
"files": ["ruby_task_helper/files/task_helper.rb"] // タスク実行時に利用するファイルの指定。基本、ヘルパーライブラリが多いかも。
}
#!/usr/bin/env ruby
require_relative '../../ruby_task_helper/files/task_helper'
require 'google/cloud/storage'
class DownloadFileFromGCS < TaskHelper
# GCSの指定バケット・オブジェクトパスを指定tmpディレクトリにダウンロードするタスク
def task(bucket_name: nil, object_path: nil, tmp_dir: nil, **_kwargs)
storage = Google::Cloud::Storage.new
bucket = storage.bucket bucket_name
object = bucket.file object_path
object.download tmp_dir + '/' + object_path
puts "Success to download #{object.name} to #{tmp_dir}"
end
end
DownloadFileFromGCS.run if __FILE__ == $0
上記カスタムタスクは、ruby_task_helper
を利用しています。名前の通り、ruby でカスタムタスクを作成する際に、いい感じにしてくれる、ruby のヘルパーライブラリです。ざっくりいうと、メタデータにパラメータを指定して、スクリプトの方で、メソッドを定義すればよい。それだけと言えばそれだけです。
ref. 利用できるタスクのデータ型
タスクの詳細は以下のように確認することもできます。
$ bolt task show myproject::download_from_gcs
myproject::download_from_gcs - Download file from google cloud storage locally
USAGE:
bolt task run --targets <node-name> myproject::download_from_gcs bucket_name=<value> object_path=<value> tmp_dir=<value>
PARAMETERS:
- bucket_name: String[1]
Bucket of google cloud storage
- object_path: String[1]
Object path of google cloud storage
- tmp_dir: String[1]
tmp path
MODULE:
/Users/jkkitakita/myproject
あとは、定義した task を直接使う、または、plan から呼び出せば ok です。
ref. Making on-demand changes with tasks
site-modules
最後に、site-modules を紹介します。site-modules は、プロジェクト毎の custom modules です。(ドキュメントがいまいちないので、合ってるか不明。私の認識です。)Puppet Forgeの puppet modules は、.modules
に格納されると上述しましたが、それでは不十分である、または、プロダクト固有の modules を作成したい場合に用意します。その際には、site-modules
ディレクトリを作成します。シンプルな plan を実行したいだけであれば、 bolt project のルートディレクトリに plan
ディレクトリを作成するだけで十分かもしれません。が、個人的には、ちゃんと今後の保守・運用も考慮するのであれば、単純な plan を実行するだけでも site-modules
ディレクトリを作成した方がいいと思います。ansible でいうと、modules
を roles
に読み替えると良いかもしません。Puppet Forge
は Ansible Galaxy
的な立ち位置なので、ansible に慣れている方は、そのイメージです。
# puppet bolt の site-modules のディレクトリ構成
...
├── site-modules
│ └── mymodule
│ ├── files
│ ├── plans
│ └── templates
...
# ref. ansible の場合の roles のディレクトリ構成
...
├── roles
│ └── myrole
│ ├── files
│ ├── tasks
│ └── templates
...
ただし、実際に使ってみた感覚から言うと、ansible の roles よりも、もう少し柔軟な(悪くいうと、フワッとした)custom modules を定義できる印象があります。具体的には
# puppet bolt の site-modules のディレクトリ構成
...
├── site-modules
│ └── mymodule
│ └── plans
│ ├── planA.yaml
│ └── planB.yaml
...
と定義した場合
# planA を実行したい場合
$ bolt plan run mymodule::planA --targets webservers
# planB を実行したい場合
$ bolt plan run mymodule::planB --targets webservers
のような形で、指定できるため、ざっくりと、サービス単位とかで、custom modules を設計することも可能です。何が言いたいかというと、ansible の場合、tasks を実行する際、 tasks/main.yaml
から実行される縛りがあるのですが、その縛りはないということです。ただ、一般的に他の puppet bolt のリポジトリを見た感じだと、ansible の roles と同様に、nginx
とかdeploy
とかの単位で分けているところが多そうには感じました。下記のリポジトリなどが参考になるかもしれません。
https://github.com/puppetlabs-seteam/control-repo
以下、site-modules 配下の files
、plans
、templates
の 3 つのディレクトリに関してのみ解説しようと思います。その他に関しては、Module structureを参考にしてください。
※ ただし、上記、Module structure
で紹介されているディレクトリ・ファイルが、site-modules
が対応しているかどうかは調べていません。全然 site-modules
に関するドキュメントがない。。)
ref. Plan location の一部に site-modules
に関して記述されています。
files
plan 実行時に、「対象ホストに、静的ファイルを upload する」「対象ホスト上で、静的スクリプトを実行する」などの場合に、その静的ファイル・スクリプトを格納するディレクトリです。ansible でも同様に files
ディレクトリを使っていますが、それと使い方は同じです。実際の bolt の plan ファイルの syntax は下記のような形になります。site-modules
やfiles
などは無視して、mymodule/motd.txt
と指定するところが少しややこしさがあるかもしれません。が、一応、他 modules の files も指定できますよ。ってことなのかなと思っています。
steps:
# motd.txt を対象ホストに upload する
- upload: mymodule/motd.txt # ref. site-modules/mymodule/files/motd.txt
destination: /etc/motd
targets: $targets
description: "Upload motd to the webservers"
# check_server.sh を対象ホスト上で実行する
# e.g. ./check_server.sh /index.html 60
- script: mymodule/check_server.sh
targets: $targets
description: "Run mymodule/files/check_server.sh on the webservers"
arguments: # Optional
- "/index.html"
- 60
ref.
- Writing plans in YAML#Script step
- Writing plans in YAML#File download step
- Writing plans in YAML#File upload step
templates
templates は、files と違い、動的に変数等を代入して、最終的にレンダリングされたファイルを生成したいファイルを管理するディレクトリです。こちらも ansible の templates と同様の用途です。ansible の場合は、python 製のため、基本的に jinja2 がテンプレートエンジンとして利用されていますが、Puppet の場合は、Embedded Puppet(EPP)、もしくは、Embedded Ruby (ERB)で記述する必要があります。以下、結論を記載します。
# mymodules/templates/env.epp
<%- | String $env,
String $gcp_project_id,
| -%>
ENV=<%= $env %>
GCP_PROJECT_ID=<%= $gcp_project_id %>
# mymodules/plans/planB.pp
#
#/tmp/.env ファイルを更新する plan
# @param targets The targets to configure
# @param env The environment
# @param tmp_dir The tmp directory for download destination
plan mymodule::planB(
TargetSpec $targets,
String[1] $env,
String[1] $tmp_dir = '/tmp'
) {
apply($targets) {
file { "${tmp_dir}/.env":
content => epp('mymodules/env.epp', {
'env' => $env,
'gcp_project_id' => lookup('gcp_project_id'), # hiera の data を取得
}),
}
}
}
上記は、それぞれ mymodules/templates/env.epp
と mymodules/plans/planB.pp
で記載した template と plan(Puppet 言語)です。これで、下記コマンドを実行してみると
bolt plan run mymodules::planB env=staging --targets webservers
centos@192.168.100.179$ cat /tmp/.env
ENV=staging
GCP_PROJECT_ID=gcp_project_staging
となります。この際、lookup('gcp_project_id')
と指定して、gcp_project_staging
が代入されています。これは puppet の build-in function lookup を使うことで、hiera data として定義した変数を取得することができるためです。
ref.
plans
bolt の plan を格納するディレクトリです。plan は、複数のタスクを一まとまりにした単位のことを呼びます。実行したタスクの入力の値を計算したり、別のタスクの結果に基づいて特定のタスクを実行したりするなど、複雑なタスクを実行できます。plan は、yaml(not .yml) or puppet(pp)で記述することが可能です。それぞれ同じで、同じタスクを記述すると
# yaml の場合(site-modules/mymodule/plans/planA.yaml)
parameters:
targets:
type: TargetSpec
bucket_name:
type: String[1]
object_path:
type: String[1]
tmp_dir:
type: String[1]
default: "/tmp"
steps:
# motd.txt を対象ホストに upload する
- upload: mymodule/motd.txt # ref. site-modules/mymodule/files/motd.txt
destination: /etc/motd
targets: $targets
description: "Upload motd to the webservers"
# check_server.sh を対象ホスト上で実行する
# e.g. ./check_server.sh /index.html 60
- script: mymodule/check_server.sh
targets: $targets
description: "Run mymodule/files/check_server.sh on the webservers"
arguments: # Optional
- "/index.html"
- 60
- task: myproject::download_from_gcs
targets: $targets
description: "Download file from gcs"
parameters:
bucket_name: $bucket_name
object_path: $object_path
tmp_dir: $tmp_dir
# puppuet の場合(site-modules/mymodule/plans/planA.pp)
# @param targets The targets to configure
# @param bucket_name The bucket name
# @param object_path The object path
# @param tmp_dir The tmp directory for download destination
plan mymodule::planA(
TargetSpec $targets,
String[1] $bucket_name,
String[1] $object_path,
String[1] $tmp_dir = '/tmp'
) {
upload_file(
'mymodule/motd.txt',
'/etc/motd',
$targets,
"Upload motd to the webservers"
)
run_script(
'mymodule/check_server.sh',
$targets,
"Run mymodule/files/check_server.sh on the webservers",
{
'arguments' => ["/index.html", 60]
}
)
run_task(
'myproject::download_from_gcs',
$targets,
'Download file from gcs',
{
'bucket_name' => $bucket_name,
'object_path' => $object_path,
'tmp_dir' => $tmp_dir,
},
)
}
となります。上記 2 つの plan は同じ plan です。細かい箇所の説明は省きますが、上記のように記述したのち
bolt plan run mymodule::planA env=staging bucket_name=mybucket object_path=myobject.txt --targets webservers
のコマンドを実行することで、対象ホスト(targets)に対して、plan を実行することができます。ちなみに余談ですが、下記コマンドを実行すると、yaml -> pp
への変換ができます。
bolt plan convert site-modules/mymodule/plans/planA.yaml
そこで次に考えるのは、yaml
とpp
どっちで書こう問題なのですが、「やっぱり yaml の方が慣れてる人多いし、puppet 独自の DSL とか学習コスト高そうだし、やっぱり yaml で書こう!」と、Puppet 言語に慣れていないほとんどの人が思うところかなと思うのですが、ここで割と大事な個人的な見解としては、「(現時点では)ちゃんと本番環境で利用とするのであれば、多少苦労してでも、Puppet言語で記述した方がいい」
です。なぜかというと、yaml だと、まだ動かないこと結構多いです。
最も致命的なのは、templateがいまいち使えない
ことです。(ref. Support Templates from yaml #2301) やはり、前身となるのが Puppet 言語なので、現段階では、「今まで、Puppet 言語で作られてきたものを YAML でも書けるようにするために色々頑張ってる」という印象が強いです。そのため現段階での個人的な見解としては、あまり yaml で記述することをお勧めしません。
さいごに
他の構成管理ツールを使ったことがある方なら、やはりディレクトリ構成も似ていたりするので、割と取っつきやすい部分もありますが、「独自言語であること」「落とし穴がある」「ドキュメント・ナレッジが少ない」の 3 つがやはりネックになってきます。上記記載したことを参考にしていただきなら、まずは、触ってみる。その中で、全体のコンセプトを掴んでもらい、設計 -> 実装とやると良いかなと思います。今後はもっと、Puppet Bolt が流行ってくることを期待したいです。
参考
- Welcome to Bolt
- その他に関しては、各項目毎の
ref
を参考にしてください。