AWS

ユーザーデータでアプリを実行するためのシェルスクリプトの書き方

tak

はじめに

こんにちは。DWS二人目の大島です。


EC2 をオートスケーリングなどで起動する際に、起動時にアプリケーションを起動してしまう、ように設定をしたいことがあると思います。


systemd などを使ってもいいのですが、DBのパスワード設定などを静的にしか行えないようなアプリケーションにおいては、パラメータストアなどから取得して設定ファイルを上書きするなどの処理が必要場合、シェルスクリプトを書いてユーザーデータで実行してしまうのが意外と楽だったりします。


しかし、ユーザーデータの実行ユーザーは root ユーザーなので、アプリ実行はアプリ実行ユーザーに切り替えて実行する必要があります。
なかなかこの辺りのシェルスクリプトの書き方などが混乱しがちなので、ユーザーの切り替えと変数の利用について、まとめました。

ユーザーデータの仕組み

まず簡単にユーザーデータの仕組みを見ておきます。

ユーザーデータ自体の仕組は非常にシンプルです。

cloud-init という EC2 を初期化するプロセスの中でシェルスクリプトとして実行されます。($0 は、実行されたスクリプトを示します)

先述の通り、ユーザーデータの実行ユーザーは root ユーザーなので、スクリプト上でファイルを作成などをすると、すべて root 権限となってしまうので注意してください。

# ユーザーデータで以下の内容を実行することで、確認ができるs

#!/bin/bash
echo $0 >> /home/ec2-user/log
pstree >> /home/ec2-user/log

# 実行結果

[ec2-user@ip-10-0-19-106 ~]$ cat log 
/var/lib/cloud/instance/scripts/part-001
systemd-+-2*[(agetty)]
        |-amazon-ssm-agen---7*[{amazon-ssm-agen}]
        |-atd
        |-auditd---{auditd}
        |-chronyd
        |-cloud-init-+-part-001---pstree
        |            `-tee
(略)

su を使ったユーザー切り替えパターン

su は指定したユーザーに切り替えるコマンドです。

su を使ったユーザーデータで最初にパッと思いつくのが↓のパターンです。

# スクリプト
su app-user
command1
command2

これはNGです。
シェルスクリプトを普段からよく使ってる人なら当たり前かもしれませんが、suは新しいインタラクティブシェルを起動します。

そのため、su 以降の行のファイル上のコマンドは、シェルスクリプトを実行している元のシェル上で実行されます。

つまり、command1, 2 は root ユーザーとして実行されてしまいます。

su の man にもきっちり su は新しいインタラクティブシェルを起動します、と記載されています。

When called with no user specified, su defaults to running an
interactive shell as root. When user is specified, additional arguments
can be supplied, in which case they are passed to the shell.

インタラクティブシェルとログインシェル

シェルにはインタラクティブシェル(interactive / non-interactive)と、ログインシェル(login / non-login)という概念があります。

インタラクティブシェル(interactive / non-interactive

インタラクティブシェルは、ユーザーがシェルに対して入力などを実行できるかシェルです。一般に利用するシェルのイメージが強いので理解しやすいかと思います。
対して、ノンインタラクティブシェルは、ユーザーの入力を受け付けず、スクリプトなどを実行するために呼び出されるシェルのことを言います。起動時に読み込まれるファイルにも違いがあります。

ログインシェル(login / non-login)

ログインシェル(login)は、一般にユーザーがログインした際に呼び出されるシェルです。各種コマンドでも --login オプションをつけることでログインシェルを呼び出すことが可能です。
ログインシェルとノンログインシェル(non-login)の大きな違いは、シェルの起動時に呼び出すファイルの違いです。

ログインシェルは、専用の各種起動ファイル(/etc/profile や~/bash_profile, ~/.bash_login, ~/.profile など)を読み込んだ上で起動します。
※読み込むファイルの詳細には触れません。

参考
GNU : 6.2 Bash Startup Files
GNU : 6.2 Bash Startup Files

su -c オプション

su には、新たに起動したインタラクティブシェルにコマンドを渡す -c というオプションがあります。
こちらを実行することで、新しく起動したシェル上でコマンドを実行することができます。

# example

su ec2-user -c "echo 'hello' >> /home/ec2-user/log"

ここまでで、ユーザーを変更してコマンドを実行ができるようになりました。
次に変数の渡し方について見ていきましょう。

su における変数の利用


su は、設定した環境変数をリセットして、一部の環境変数(HOME, SHELL, USER, LOGNAME, PATH など)だけを設定するので、単に環境変数をセットするだけでなく、もう一手間加える必要があります。

  su does:

           •   clears all the environment variables except TERM and
               variables specified by --whitelist-environment

           •   initializes the environment variables HOME, SHELL, USER,
               LOGNAME, and PATH
man su より:https://man7.org/linux/man-pages/man1/su.1.html

変数の利用においては、いろいろパターンがあるとは思いますが、ここではざっくり3つ取り扱います。

  • シェルスクリプト上の変数を利用する
  • コマンド実行時に変数を渡す
  • 環境変数を引き継ぐオプションを利用する

シェルスクリプト上の変数を利用する

一番簡単な方法はシェルスクリプト上の変数を利用してしまうことかと思います。

「"(ダブルクォート)」で文字列を囲った場合、その中にある変数はシェルスクリプト上で評価され展開されるので、変数の展開ができます。

# シェルスクリプト
MYENV=hello
su ec2-user -c "echo $MYENV"

# 結果
hello

ただ、一部起動時に環境変数として設定してやらなければならないものもあったりするので、その場合には、次項の起動時に環境変数として指定する方法と合わせる必要があります。

# シェルスクリプト
DB_PASSWORD=hogehoge
su - ec2-user -c "
DB_PASSWORD=$DB_PASSWORD start_some_app" # 環境変数にシェルスクリプト上の変数を展開して渡す

コマンド実行時に変数を引き渡す

コマンド実行時に、環境変数を指定することも可能です。
こちらは先ほどの例と違い、su -c で渡したコマンドの中で実際に変数を利用したい場合「'(シングルクォート)」でないと動作しません。

というのも、「"」だと即座に$MYENVの評価がなされるため、環境変数が設定される前の状態で、$MYENVが評価されます。(「”」の場合、「\」などでエスケープが必要です。)

# コマンド
MYENV=hello su ec2-user -c 'echo $MYENV'

# 結果
hello
ダブルクォートとシングルクォート
  • シングルクォートの場合、一旦文字列リテラルとして解釈されるので、"$MYENV"のまま渡されます。つまり、新しいシェルに渡ったとき、初めて変数として解釈されるので、シェル上のシェル変数・環境変数として理解されます。
  • ダブルクォートの場合、変数は文字列として展開された上でシェル上に渡されます。つまり、シェル上にはファイル上の変数が展開されて渡されるので、文字列が渡されます。

環境変数を引き継ぐオプション(-m)を利用する

su には、現在のシェルの環境変数を引き継ぐための -m というオプションがあります。
こちらのオプションを利用すれば、現在のシェル上で普通に環境変数を宣言して利用できるので、実行可能です。

 Preserve the entire environment, i.e., do not set HOME, SHELL, USER or LOGNAME. This option is ignored if the option --login is specified.
man su より:https://man7.org/linux/man-pages/man1/su.1.html

# コマンド
export MYENV=hello
su ec2-user -m -c 'echo $MYENV'


# 結果
hello
シェル変数と環境変数

シェル変数は、そのシェルのプロセスの中でだけ有効な変数です。子プロセスには引き継がれません。

環境変数は、親プロセスから fork() によって子プロセスが生成された時に、そのまま引き継がれます。つまり、基本的に親プロセスが同じ場合、環境変数が同じになります。

sudo を利用するパターン

sudo では、su とは違い、そのまま実行したシェルでコマンドが実行されるので、シンプルです。

さらに sudo で変数を利用してコマンドを実行する場合、「"」や「'」が利用できるわけではなく直接記述することになり、sudo が実行される環境での環境変数がそのまま利用されます。
裏側では sudo はセキュリティの関係上、環境変数をリセットしてしまうので、注意は必要ですが、あまり意識せずに利用が可能かと思います。(必要であれば、-E オプションを使うことで、引き継ぐこともできます)

# 以下のように記述する場合、変数は sudo 実行時の環境で評価されるため、
# コマンド
export MYENV=hello
sudo echo $MYENV

# 結果
hello

# 以下のように環境変数自体はリセットされるため、env などを実行すると表示されない
# コマンド
export MYENV=hello
sudo env | grep MYENV

# 結果は何も表示されない

# -E オプションを使うと環境変数を引き継げる
# コマンド
export MYENV=hello
sudo -E env | grep MYENV

# 結果
MYENV=hello


おわりに

簡単にですが、ユーザー変更を伴うユーザーデータ実行について記述させていただきました。

どなたかの役に立てば幸いです。

AUTHOR
tak
tak
記事URLをコピーしました