【Vagrant +CentOS】シェルスクリプトを使用して自動で環境構築を行う

【Vagrant +CentOS】シェルスクリプトを使用して自動で環境構築を行う

先日 XHACK 勉強会にて Linux のコマンドやシェルスクリプトに関する勉強会と、正規表現に関する勉強会に参加しました。

そのイベントで学習したことをもとに、面倒な環境構築を自動化するシェルスクリプトを組んだのでそれについて記述していきます。

この記事では Vagrant + CentOS での環境下で、LAMP 環境 (Linux、Apache、MySQL、PHP)を作っていきます。

インストールしているバージョンは以下のようになります。

VirtualBox:6.1.18

Vagrant:2.2.14

CentOS:8.3

Apache:2.4.37

PHP:7.4.16

MySQL:8.0.21

設定している内容に関しては、過去の記事と同様のものにしています。

はじめに

参加した勉強会はこちら。

登壇者:がきさん( https://twitter.com/gakisan8273

登壇者:たなべこうせいさん( https://twitter.com/Mario02108813

参加したきっかけ

Linux コマンドに関しては、よく使用するコマンドの使い方などは知っていたものの、あまり深く追求してこなかったのでとりあえずコピペして使う程度のものでした。

そして、Web 開発をしていると必ずと言って Linux コマンドには触れることになりますし、開発環境の構築をする際にも使用するので、一度基礎から学習しておこう!と思ったのがきっかけです。

正規表現に関しては、現在の仕事で使えたらデータの集計や面倒な作業を自動化できそうだな〜と思っていて、タイムリーな勉強会だったので速攻で参加しました!!

それでは勉強会の振り返りも兼ねて今回作成したスクリプトを見ていきます。

シェル?シェルスクリプト?

シェルとは、ユーザーがキーボードなどで入力した命令をコンピューターが分かる用に伝えるソフトウェアのことです。

もう少し詳細に言うと、シェルは OS の中核であるカーネルにターミナルから入力したコマンド(命令)を伝え、そのカーネルの実行結果をターミナルに出力する役割を担っています。

人とコンピューターの橋渡しをしてくれるようなヤツです。

シェルスクリプトとは、そのコマンドをファイルにまとめて実行できるようにしたスクリプト言語です。ある条件の時にコマンドを実行したり、ログファイルを書き出したりすることが出来ます。

正規表現とは?

正規表現とは条件に一致する複数の文字列を一つの形式で表現する方法のことです。

例えば、「末尾が abc で終わる文字列」や「連続した数字を 7 個持っている文字列」などのような条件を指定してマッチする文字列を得ることが出来ます。

.(ドット)や *(アスタリスク)などのメタ文字と呼ばれる特殊な記号を使って検索したい文字列を指定します。

こちらのツールを使うと簡単に検証できるので便利です。

https://rakko.tools/tools/57/

マッチする文字列を検索する際の一例です。

# 正規表現
# .(ドット)は何でも良い一文字を表す

私は.です。

# 検索文字列
私は犬です。
私は鳥です。
私はキジです。

# 結果
私は犬です。
私は鳥です。


# ドットを1つ増やしてみる
私は..です。

# 結果
私はキジです。

他にも色々なメタ文字があります。

THTH
\直後に続く演算文字( + や / など)をメタ文字ではなくリテラルとして解釈する
.任意の文字を表す
^検索対象の始まり。または文字クラスの否定
+直前の文字列が 1 個以上あるかチェックする
$検索対象の終わり
[文字クラスの始まり
]文字クラスの終わり
*直前の文字列が 0 回以上繰り返されているか判定
?直前の文字列が 0 個か 1 個以上繰り返されているか判定
OR 検索を行う(○○ または XX が含まれていればマッチする)
{n,m}直前の文字の繰り返し回数を指定する(n は最小値、m は最大値)

シェルスクリプトと組み合わせて全て自動化したい

今まで環境構築を行うときは、予め手順をまとめておいてコマンドをコピペしてターミナルに打ち込んでいました。

一回で終わればそれて良いのですが、何かと作っては潰すのが多いのがローカル開発環境だと思います。

作り直すの面倒ですよね?

これ自動化したい!とずっと思っていました。ただ設定ファイルなどもあるのでコマンドを打ち込めば良い訳ではありません。

完全に自動化するには設定ファイルの中身を読み取り、任意の文字列を書き換えるという高度(?)な処理が必要になります。

実際のシェルスクリプト

実際のコードはこちらです。(けっこう長いです。。。)

#!/bin/bash

# testコマンドを使った書き方
# if test -f /home/vagrant/bootstrapped ; then
if [ -f /home/vagrant/bootstrapped ]; then
echo "[INFO] 全ての設定が完了しています。"
else

# 初回に起動時にCentOSを最新の状態にしておく
function os_update()
{
 if [ -f /home/vagrant/os_update ]; then
  echo "[INFO] CentOSは最新です"
 else

  dnf -y upgrade
  # 処理の終了コードを取得
  RESULT=$?
  # 結果のチェック
  if [ $RESULT -eq 0 ]; then
    echo "[INFO] 最新のCentOSに更新されました。vagrant reloadコマンドを実行して再起動してください。"
    date > /home/vagrant/os_update
    exit 0;
  else
    error "[ERROR] os_updateでエラーが発生 異常終了"
    exit 0;
  fi
 fi
}

function additional_package()
{
 if [ -f /home/vagrant/additional_package_done ]; then
  echo "[INFO] additional_package 既に設定済みです"
  return 0
 fi

 dnf -y install vim
 dnf -y install git
 dnf -y install wget
 dnf -y install unzip

 # 処理の終了コードを取得
 RESULT=$?
 # 結果のチェック
 if [ $RESULT -eq 0 ]; then
  echo "[INFO] 処理終了"
  date > /home/vagrant/additional_package_done
  return 0
 else
  error "[ERROR] 予期せぬエラーが発生 異常終了"
  return 1
 fi
}

function apache_install_and_setting_do()
{
 if [ -f /home/vagrant/php_install_and_setting_done ]; then
  echo "[INFO] apache_install_and_setting_do 既に設定済みです"
  return 0
 fi

 dnf install -y httpd httpd-tools httpd-devel httpd-manual
 systemctl start httpd
 systemctl enable httpd
 cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/httpd.conf_old
 sed -i -e "s/#ServerName.*w.*/ServerName www.example.com:80/" /etc/httpd/conf/httpd.conf
 sed -e 's/\(.*\)Indexes\(.*\)/\1\2/g' /etc/httpd/conf/httpd.conf

 # 処理の終了コードを取得
 RESULT=$?
 # 結果のチェック
 if [ $RESULT -eq 0 ]; then
  echo "[INFO] 処理終了"
  date > /home/vagrant/apache_install_and_setting_done
  return 0
 else
  error "[ERROR] apache_install_and_setting_doでエラーが発生 異常終了"
  return 1
 fi
}

function php_install_and_setting_do()
{
 if [ -f /home/vagrant/php_install_and_setting_done ]; then
  echo "[INFO] php_install_and_setting_do 既に設定済みです"
  return 0
 fi

 dnf install -y httpd httpd-tools httpd-devel httpd-manual
 cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/httpd.conf_old
 dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
 dnf -y install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
 dnf module disable php
 dnf module install -y php:remi-7.4
 dnf install -y php-mysqlnd php-xmlrpc php-pear php-gd php-pdo php-intl php-mysql php-mbstring
 cp /etc/php.ini /etc/php.ini_old

# ヒアドキュメント
cat >> /etc/php.ini << "EOF"
date.timezone = Asia/Tokyo
mbstring.language = Japanese
mbstring.internal_encoding = UTF-8
mbstring.http_input = UTF-8
mbstring.http_output = pass
mbstring.encoding_translation = On
mbstring.detect_order = auto
mbstring.substitute_character = none
upload_max_filesize = 128M
post_max_size = 128M
EOF

 cp /etc/php-fpm.d/www.conf /etc/php-fpm.d/www.conf_old
 # 指定した文字列の先頭をコメントアウトする
 sed -i -e "/listen\..*_.*apache/s/^/;/g" /etc/php-fpm.d/www.conf
 # ()で指定した箇所は残して一部分を置き換える
 sed -i -e "s/\(.*\) = 50$/\1 = 25/g" /etc/php-fpm.d/www.conf
 sed -i -e "s/\(.*\) = 5$/\1 = 10/g" /etc/php-fpm.d/www.conf
 sed -i -e "s/\(.*\) = 35$/\1 = 20/g" /etc/php-fpm.d/www.conf
cat >> /etc/php-fpm.d/www.conf << "EOF"
listen.owner = apache
listen.group = apache
listen.mode = 0660
pm.max_requests = 500
EOF
 systemctl start php-fpm
 systemctl enable php-fpm

 # 処理の終了コードを取得
 RESULT=$?
 # 結果のチェック
 if [ $RESULT -eq 0 ]; then
  echo "[INFO] 処理終了"
  date > /home/vagrant/php_install_and_setting_done
  return 0
 else
  error "[ERROR] php_install_and_setting_doでエラーが発生 異常終了"
  return 1
 fi
}

function mysql_install_and_setting_do()
{
 if [ -f /home/vagrant/mysql_install_and_setting_done ]; then
  echo "[INFO] mysql_install_and_setting_do 既に設定済みです"
  return 0
 fi

 dnf install -y @mysql
 systemctl start mysqld
 systemctl enable mysqld

 if [ -f /etc/my.cnf.d/common.cnf ]; then
  echo "[INFO] mysqlは既に設定済みです"
 else
  touch /etc/my.cnf.d/common.cnf
  cat >> /etc/my.cnf.d/common.cnf << "EOF"
# 文字コード設定/照合順序設定
[mysqld]
collation_server = utf8mb4_ja_0900_as_cs_ks
EOF
  systemctl restart mysqld
  systemctl status mysqld
 fi

# パッケージをダウンロード済みの場合は以下の処理を行わない
 if [ -f /home/vagrant/phpMyAdmin-5.0.2-all-languages.zip ]; then
  echo "[INFO] phpMyAdminは既に設定済みです"

 else
  wget https://files.phpmyadmin.net/phpMyAdmin/5.0.2/phpMyAdmin-5.0.2-all-languages.zip
  unzip phpMyAdmin-5.0.2-all-languages.zip
  mv phpMyAdmin-5.0.2-all-languages /usr/share/phpMyAdmin
  cp -pr /usr/share/phpMyAdmin/config.sample.inc.php /usr/share/phpMyAdmin/config.inc.php
  touch /etc/httpd/conf.d/phpMyAdmin.conf
  cat >> /etc/httpd/conf.d/phpMyAdmin.conf << "EOF"
Alias /phpMyAdmin /usr/share/phpMyAdmin
Alias /phpmyadmin /usr/share/phpMyAdmin
 <Directory /usr/share/phpMyAdmin/>
  AddDefaultCharset UTF-8
   <IfModule mod_authz_core.c>
   #Apache 2.4
   <RequireAny>
   Require all granted
   </RequireAny>
   </IfModule>
 </Directory>
 <Directory /usr/share/phpMyAdmin/setup/>
  <IfModule mod_authz_core.c>
   #Apache 2.4
   <RequireAny>
    Require all granted
   </RequireAny>
  </IfModule>
</Directory>
EOF

  # 指定した文字列の先頭をコメントアウトする
  sed -i -e "s/.*blowfish_secret.*/\/\/&/g" /usr/share/phpMyAdmin/config.inc.php
  # マッチした文字列の次の行に指定した文字列を追加
  sed -i -e "/.*blowfish_secret.*/a \$cfg['blowfish_secret'] = '任意のパスワード';" /usr/share/phpMyAdmin/config.inc.php
 fi

 if [ -f /home/vagrant/file_bundled_sql ]; then
  echo "[INFO] mysql自動接続用のファイルは既に存在しています"
 else
  touch /home/vagrant/file_bundled_sql
  cat >> /home/vagrant/file_bundled_sql << "EOF"
use mysql;
ALTER USER 'root'@'localhost' IDENTIFIED BY 'phpMyAdmin/config.inc.phpで指定したものと同じパスワード';
EOF
 fi

 mysql -u root --password= < '/home/vagrant/file_bundled_sql'

 # 処理の終了コードを取得
 RESULT=$?
 # 結果のチェック
 if [ $RESULT -eq 0 ]; then
  echo "[INFO] 処理終了"
  date > /home/vagrant/mysql_install_and_setting_done
  return 0
 else
  error "[ERROR] mysql_install_and_setting_doでエラーが発生 異常終了"
  return 1
 fi
}

function confirm_service_status()
{
 systemctl restart httpd
 systemctl restart mysqld
 systemctl restart php-fpm

 # 処理の終了コードを取得
 RESULT=$?
 # 結果のチェック
 if [ $RESULT -eq 0 ]; then
  echo "[INFO] 処理終了"
  return 0
 else
  error "[ERROR] confirm_service_statusでエラーが発生 異常終了"
 return 1
 fi
}

function creating_phpinfo_file()
{
 if [ -f /home/vagrant/creating_phpinfo_file_done ]; then
  echo "[INFO] creating_phpinfo_file 既に設定済みです"
  return 0
 fi
 touch /var/www/html/index.php
 cat >> /var/www/html/index.php << "EOF"
<?php
phpinfo();
?>
EOF

 # 処理の終了コードを取得
 RESULT=$?
 # 結果のチェック
 if [ $RESULT -eq 0 ]; then
  echo "[INFO] 処理終了"
  date > /home/vagrant/creating_phpinfo_file_done
  return 0
 else
  error "[ERROR] confirm_service_statusでエラーが発生 異常終了"
  return 1
 fi
}

# 標準出力を標準エラーに上書きしてメッセージを表示
function error()
{
 echo "$@" 1>&2
 exit 1
}

# 関数を実行する
os_update
additional_package
apache_install_and_setting_do
php_install_and_setting_do
mysql_install_and_setting_do
creating_phpinfo_file
confirm_service_status

date > /home/vagrant/bootstrapped
fi

条件分岐

シェルスクリプトでの条件分岐は以下の用に記述します。ここではファイルが存在しているか否かで真偽を判定しています。

# testコマンドを使った書き方
if test -f /home/vagrant/bootstrapped ; then
  # trueの処理
else
  # falseの処理
fi

# testコマンドを省略した書き方(testコマンドを使ったのと同じ)
if [ -f /home/vagrant/bootstrapped ]; then
 # trueの処理
else
 # falseの処理
fi

test コマンドは、条件式を判断して真偽値を返すコマンドです。この結果により条件分岐させることが出来ます。

コマンドを省略して、かっこ [ ] で記述する方法もあります。この時の注意点として、かっこの直後・直前に半角スペースが必要だという事です。

オプション -f を付けることで、そのファイルがファイルが通常のファイルのときに真を返します。

通常ファイルとはディレクトリや特殊ファイル以外のもで、「ls -l」で情報を表示した時に先頭が

「 – 」と表示されているのもが通常ファイルとなります。

# スクリプト実行後のディレクトリ /home/vagrant

mkdir test

ls -l

total 13900
-rw-r--r--. 1 root    root          29 Apr 19 14:11 additional_package_done
-rw-r--r--. 1 root    root          29 Apr 19 14:12 apache_install_and_setting_done
-rw-r--r--. 1 root    root          29 Apr 19 15:19 bootstrapped
-rw-r--r--. 1 root    root          29 Apr 19 14:42 creating_phpinfo_file_done
-rw-r--r--. 1 root    root          70 Apr 19 14:42 file_bundled_sql
-rw-r--r--. 1 root    root          29 Apr 19 14:42 mysql_install_and_setting_done
-rw-r--r--. 1 root    root          29 Apr 19 14:03 os_update
-rw-r--r--. 1 root    root    14199213 Mar 21  2020 phpMyAdmin-5.0.2-all-languages.zip
-rw-r--r--. 1 root    root          29 Apr 19 14:36 php_install_and_setting_done
-rw-r--r--. 1 root    root           0 Apr 19 15:19 script_logs
drwxrwxr-x. 2 vagrant vagrant        6 Apr 19 15:37 test

# ディレクトリは通常ファイルではない

test コマンドの実行結果は、コマンド実行後に echo $? で確認することが出来ます。0 であれば真で 1 であれば偽という結果になります。

# スクリプト終了後に作成されるファイル
test -f /home/vagrant/bootstrapped
echo $?
0

# 存在しないファイルを指定
test -f /home/vagrant/hogehogehoge
echo $?
1


# 作成したディレクトリを指定
test -f /home/vagrant/test
echo $?
1

exit と return の違い

exit は実行した時点でスクリプトが終了します。引数の終了ステータスを指定する事で、正常終了か異常終了かステータスを指定することが出来ます。

ステータスは 0〜255 の任意の整数値を指定可能で、0 は正常に終了した場合に返す終了ステータスで、1 はエラーが発生した場合に返す終了ステータスにに指定するのが慣例となっているようです。

return は実行しているシェル関数を終了し、シェル関数の呼び出し元に戻ります。実行中の関数を終了するので後続に処理があれば実行されます。

# 処理の終了コードを取得
RESULT=$?

# 結果のチェック
if [ $RESULT -eq 0 ]; then

  echo "[INFO] 処理終了" 
  date > /home/vagrant/creating_phpinfo_file_done
  return 0

else

  error "[ERROR] confirm_service_statusでエラーが発生 異常終了"
  return 1

fi

### 中略 ###

# 標準出力を標準エラーに上書きしてメッセージを表示
function error()
{
 echo "$@" 1>&2
 exit 1

sed コマンドと正規表現を利用してコンフィグを書き換える

sed コマンドは文字列を置換、行の削除、追加などコマンド一つでテキストに対して色々な処理を行うことが出来ます。これを利用して Apache や PHP のコンフィグファイルを書き換えていきます。

オプションは以下のようなものが指定できます。

THTH
aテキストの追加。指定した位置の後ろに[テキスト]を挿入する
d指定した行を削除する
iファイルの上書き更新を行う
s/置換前/置換後/[置換前]で指定した文字列にマッチした部分を[置換後]に置き換える。複数マッチした場合は先頭のみ置換、全てを置換したい場合は、「s/置換前/置換後/g」のように「g」オプションを指定する

今回実行したコマンドを例に見ていきましょう。

# 特定の文字列を切り出す
sed -e 's/\(.*\)Indexes\(.*\)/\1\2/g' /etc/httpd/conf/httpd.conf

# 指定した文字列の先頭をコメントアウトする
sed -i -e "s/.*blowfish_secret.*/\/\/&/g" /usr/share/phpMyAdmin/config.inc.php

# 指定した文字列の先頭をコメントアウトする
sed -i -e "/listen\..*_.*apache/s/^/;/g" /etc/php-fpm.d/www.conf

文字列のある一部分を抽出して置換する

ある一部分の文字列を置換する場合は以下のように実行します。

# Indexes だけ消したい
Options Indexes FollowSymLinks

# \(.*\)が維持されてIndexesだけが空文字として上書きされる
sed -i -e 's/\(.*\)Indexes\(.*\)/\1\2/g' /etc/httpd/conf/httpd.conf

# 実行後
Options FollowSymLinks

# 置換後の文字列を追加する場合
sed -i -e 's/\(.*\)Indexes\(.*\)/\1\2 test/g' /etc/httpd/conf/httpd.conf

# 実行後
Options FollowSymLinks test

動作としては、(.*) で囲まれている Indexes という文字列のみ切り抜かれて、その前後の文字列は維持されます。

\1 と指定すると(.*) にマッチした文字列が格納され維持されます。複数指定する場合は\1\2 の用に記述し、ここでは \1\2 と指定しているので、前後のマッチした文字列は維持されます。

\1\2 の後に置換後の文字列を指定出来ますが、囲んだ文字列を切り抜きたいだけなので何も指定していません。

また、\1\2 の間に置換後の文字列を指定することによって、切り出した文字列の間に挿入することが出来ます。

# 置換後の文字列を追加する場合(半角スペースは開けなくても良い)
sed -i -e 's/\(.*\)Indexes\(.*\)/\1test\2/g' /etc/httpd/conf/httpd.conf

# 実行後
Options test FollowSymLinks

特定の行をコメントアウトする

これも様々な方法がありますが、その 1 では置換後の文字列に &(アンパサンド)を使用するやり方で実装します。この & には正規表現でマッチした文字列が格納されるので、マッチした文字列の先頭にスラッシュを付けてコメントアウトしています。

その 2 では、listen..*_.*apache にマッチした行に対して先頭に文字列を追加しています。どちらもやっていることは同じですが、マッチした文字列の扱い方が違うことが分かります。

^(キャレット)を$(ダラー)に変えると、行末に文字列を追加するという動きになります。

# その1
# &には .*blowfish_secret.* でマッチした文字列が格納されている
sed -i -e "s/.*blowfish_secret.*/\/\/&/g" /usr/share/phpMyAdmin/config.inc.php

# その2
# 特定の文字列を含む行に対して置換をする
sed -i -e "/listen\..*_.*apache/s/^/;/g" /etc/php-fpm.d/www.conf

# ^を$に変えると行末に追加するという意味になる
sed -i -e "/listen\..*_.*apache/s/$/;/g" /etc/php-fpm.d/www.conf

まとめ

今回の勉強会に参加するまでは、正規表現に関して調べてもいまいち勝手がわからずかなり苦手意識がありました。

勉強会ではその記号が何を意味しているのか、実際にはどのように使用されているのか具体例をみて手を動かしながら検証していったので、すんなりと理解することが出来ました。

Linux コマンドに関しても、パスの通し方やパーミッション関する知識、その他知らないコマンドやその動作について知ることが出来たので非常に面白い勉強会でした!

勉強会で学んだことを元に、面倒なことを自動化するスクリプトを作成することが出来たのでいろいろ応用していきたいですね。

参考

https://bi.biopapyrus.jp/os/linux/sed.html

https://orebibou.com/ja/home/201507/20150731_001/