先日 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 個持っている文字列」などのような条件を指定してマッチする文字列を得ることが出来ます。
.(ドット)や *(アスタリスク)などのメタ文字と呼ばれる特殊な記号を使って検索したい文字列を指定します。
こちらのツールを使うと簡単に検証できるので便利です。
マッチする文字列を検索する際の一例です。
# 正規表現
# .(ドット)は何でも良い一文字を表す
私は.です。
# 検索文字列
私は犬です。
私は鳥です。
私はキジです。
# 結果
私は犬です。
私は鳥です。
# ドットを1つ増やしてみる
私は..です。
# 結果
私はキジです。
他にも色々なメタ文字があります。
TH | TH |
---|---|
\ | 直後に続く演算文字( + や / など)をメタ文字ではなくリテラルとして解釈する |
. | 任意の文字を表す |
^ | 検索対象の始まり。または文字クラスの否定 |
+ | 直前の文字列が 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 のコンフィグファイルを書き換えていきます。
オプションは以下のようなものが指定できます。
TH | TH |
---|---|
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 コマンドに関しても、パスの通し方やパーミッション関する知識、その他知らないコマンドやその動作について知ることが出来たので非常に面白い勉強会でした!
勉強会で学んだことを元に、面倒なことを自動化するスクリプトを作成することが出来たのでいろいろ応用していきたいですね。