ちょっと話題の記事

実践Cloud DIパターン – 環境ごとにApache設定ファイルを読み分ける

2015.04.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

Cloud DIパターンというCloud Design Pattern(CDP)があります。

AWSではAMIで好きなサーバをいくらでも増やすことが可能です。ですが、例えば開発環境のサーバでAMIを取得し、それを本番環境用のサーバとして起動したとします。果たしてそのサーバは正常に機能するでしょうか。恐らく多くのパターンでは本番サーバとして振る舞うことができないのではないかと思います。理由は単純で、サーバの設定が開発環境用になっているからですね。

せっかくAMIでサーバを複製できるので、開発環境と本番環境がひとつのマシンイメージから起動できたら非常に楽になると思います。それを実現するための一つのパターンがCloud DIです。EC2のタグに記載された情報を読み込み、それを利用してサーバの振る舞いを変化させる手法です。サーバの内部を変えずに外側から設定を注入するのがDependency Injectionっぽいですね。

Cloud DIはたいへんに便利なので是非もっと知られてほしいと思いこのブログを書いています。Cloud DIの実践編として、いくつか例をブログに書いていきたいと思います。第1回の今回はApacheの設定を切り替える方法を書いていきます。

前提

以下のような状況を想定しています。

  • ApacheのインストールされたEC2がある。
  • 開発環境と本番環境の2環境存在する
  • 開発環境と本番環境の表示出し分けはApacheの設定で実現したい

なお、EC2のOSはAmazon Linuxを想定しています。

実装(EC2タグ)

まずはCloud DIの実現に必要なEC2タグを設定しましょう。今回は開発環境と本番環境で設定を変えたいのでEnvironmentタグをつけ、開発環境にはdev、本番環境にはprodというValueを設定することにしましょう。写真ではEnvironmentタグにprodをつけているので、このEC2は本番環境として振る舞うことになります。

EC2_Management_Console

実装(EC2内)

Apacheの設定をサーバによって分けるので、環境に依存する設定は別のファイルに分割しておき、本体のhttpd.confからIncludeで読み込む形を作りましょう。

まず、/etc/httpd/conf.d/virtualhost/というディレクトリを作成し、そこに以下のようにファイルを作成します。

[ec2-user@ip-172-31-20-49 virtualhost]$ ls /etc/httpd/conf.d/virtualhost/
virtualhost.dev.conf  virtualhost.prod.conf
[ec2-user@ip-172-31-20-49 virtualhost]$ cat /etc/httpd/conf.d/virtualhost/virtualhost.dev.conf
<VirtualHost *:80>
	DocumentRoot /var/www/html/dev/
	DirectoryIndex index.html
</VirtualHost>
[ec2-user@ip-172-31-20-49 virtualhost]$ cat /etc/httpd/conf.d/virtualhost/virtualhost.prod.conf
<VirtualHost *:80>
	DocumentRoot /var/www/html/prod/
	DirectoryIndex index.html
</VirtualHost>

環境ごとに異なるVirutalHost設定を実施し、DocumentRootを変えています。

そして、/etc/httpd/conf/httpd.confに以下の記載を追加します。

Include conf.d/virtualhost/virtualhost.${HTTPD_ENVIRONMENT}.conf

記載の通り、httpdは環境変数によって開発環境か本番環境かを判断することになります。そのため、Apacheの起動前にEC2のタグを読み取って環境変数を設定しておく必要があります。

その仕掛けは起動スクリプトに書いておくのがベストでしょう。/etc/init.d/httpdですね。起動スクリプトのstart()の中に以下の記述を仕込みましょう。

start() {
        instanceId=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
        region=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed -e 's/.$//')
        tags=$(aws ec2 describe-tags --region ${region} --filters \
            "Name=resource-type,Values=instance" \
            "Name=resource-id,Values=${instanceId}")
        export HTTPD_ENVIRONMENT=$(echo ${tags} | jq -r '.Tags[] | select(.Key == "Environment") | .Value')

EC2のインスタンスメタデータからインスタンスIDと稼働リージョンを取得し、自分自身のタグを取得します。そこで取得したEnvironmentタグの値をHTTPD_ENVIRONMENT環境変数としてexportしたあとにhttpdを起動しています。

以上でApacheの設定ファイル切り分けは完了です。

動作確認

動作確認をしてみましょう。今回はそれぞれのDocumentRootに、環境名を記載しただけのindex.htmlを用意しています。

まずは本番環境(Environment=prod)のアクセス確認をしましょう。

[ec2-user@ip-172-31-20-49 ~]$ sudo service httpd start
Starting httpd:                                            [  OK  ]
[ec2-user@ip-172-31-20-49 ~]$ curl http://localhost/
prod

しっかり本番環境用の設定が効いていますね。では次にEC2のタグをprodからdevに切り替えます。

EC2_Management_Console

その後Apacheを再起動し、再度リクエストを飛ばしてみます。

[ec2-user@ip-172-31-20-49 ~]$ sudo service httpd restart
Stopping httpd:                                            [  OK  ]
Starting httpd:                                            [  OK  ]
[ec2-user@ip-172-31-20-49 ~]$ curl http://localhost/
dev

開発環境用の設定に切り替わりました!バッチリですね。

注意事項

当然ですが、AWSのAPI呼び出しはHTTPリクエストになるため、失敗する可能性を考慮に入れておかなければいけません。今回で言えば自分自身のEC2タグを取得する部分ですね。ある程度の失敗は考慮に入れ、リトライ処理を入れておいたほうがよいでしょう。bashスクリプトでは以下のような感じになります。

getInstanceTagWithRetry() {
    attempts=0
    maxRetry=5
    resultFile=/tmp/describe-tags-result

    while :; do
        aws ec2 describe-tags --region ${region} --filters \
            "Name=resource-type,Values=instance" \
            "Name=resource-id,Values=${instanceId}" > ${resultFile}
        # if API call fails
        if [ $? -ne 0 ]; then
            # if attempts exceeded maxRetry
            if [ "${attempts}" -ge "${maxRetry}" ]; then
                log "failed to get Instance tags"
                exit 1
            fi
        else
            # if API call succeeds
            break
        fi
        attempts=$((attempts + 1))
        sleep 10
    done
}

まとめ

Cloud DIの考え方と使い方について簡単に紹介しました。プログラムから扱うことになるのでCloud DIの応用範囲はとても広く、様々な活用ができます。今後、今回紹介した例の他にもCloud DIの使い方をブログに書いていきたいと思います。