スポンサーリンク

AWSのCloudFormationでマルチAZなWordPressを構築する(S3,goofys)

前回前々回はシンプルなWordPressを構築するサンプルテンプレートの修正を行ってきました。
今回は少しステップアップしてマルチAZなサンプルテンプレートの修正に挑戦してみました。
[マルチAZなサンプルテンプレート]

  • 前回のシンプルなWordPressの構築手順と同様に最新のWordPressに対応したLAMP環境を構築
    • インストールディレクトリ等の設定も同様に行えるようにする
  • WordPressのuploadsディレクトリをEC2の複数台構成に対応させる
    • サンプルテンプレートは標準でアプリケーションロードバランサー、ターゲットグループ、起動設定の辺りを作成構築するようになっているがWordPressのアップロードディレクトリについて特に共有の設定が行われていない
      • このままスケーリングポリシーによるインスタンスの増減とかを行ってしまうとアップロードしたファイルが参照できないとか消失するという現象が発生すると考えられる
      • goofysをインストールしS3をWordPressのアップロードディレクトリとして使うような設定を追記する

シンプルなWordPressスタックとの構成を比較する

以下はシンプルなWordPressスタックのテンプレートをデザイナ画面にコピペしたときの構成図です。セキュリティグループとEC2インスタンスが1対1で分かりやすくシンプルです。

以下は今回構築するWordPressスタックの構成図です。左上の方に4つ固まってるS3関連のリソースは今回私が独自に追加したリソースになります。
ロードバランサーとかターゲットグループの辺りは単体では動作しないのでどうしてもリソースの数はこんな感じで増えてしまう模様。
リソースからリソースに紐づいてる矢印はRef関数による参照などを元に出来上がってる模様。リソースの関係がわかりやすくなるので良い。

S3バケットを構築する

WordPressのuploadsディレクトリをS3で共有するため、S3バケット、S3バケットポリシー、IAMユーザー、IAMユーザーアクセスキーの4リソースを新規追加しました。
IAMユーザーはS3をgoofysでマウントするために追加しました。作業中にいろいろ調べた際、IAMのユーザーをS3のために作るみたいな情報はあまり見つからなかったので、もしかしたらユーザーを作らないでどうにかする方法がある??
S3関係の4リソースに関するテンプレート設定は以下のような感じです。

{
    "Resources": {
        "S3Bucket": {
            "Type": "AWS::S3::Bucket",
            "Properties": {
                "BucketName": {
                    "Ref": "S3BucketName"
                },
                "AccessControl": "Private"
            }
        },
        "S3BucketPolicy": {
            "Type": "AWS::S3::BucketPolicy",
            "Properties": {
                "Bucket": {
                    "Ref": "S3Bucket"
                },
                "PolicyDocument": {
                    "Statement": {
                        "Effect": "Allow",
                        "Action": [
                            "s3:PutObject",
                            "s3:GetObject",
                            "s3:DeleteObject",
                            "s3:ListBucket"
                        ],
                        "Resource": [
                            {
                                "Fn::Join": [
                                    "",
                                    [
                                        "arn:aws:s3:::",
                                        {
                                            "Ref": "S3BucketName"
                                        },
                                        "/*"
                                    ]
                                ]
                            },
                            {
                                "Fn::Join": [
                                    "",
                                    [
                                        "arn:aws:s3:::",
                                        {
                                            "Ref": "S3BucketName"
                                        }
                                    ]
                                ]
                            }
                        ],
                        "Principal": {
                            "AWS": {
                                "Fn::GetAtt": [
                                    "S3BucketUser",
                                    "Arn"
                                ]
                            }
                        }
                    }
                }
            }
        },
        "S3BucketUser": {
            "Type": "AWS::IAM::User",
            "Properties": {
                "Path": "/",
                "Policies": [
                    {
                        "PolicyName": {
                            "Fn::Join": [
                                "",
                                [
                                    {
                                        "Ref": "S3BucketName"
                                    },
                                    "User"
                                ]
                            ]
                        },
                        "PolicyDocument": {
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "s3:PutObject",
                                        "s3:GetObject"
                                    ],
                                    "Resource": {
                                        "Fn::Join": [
                                            "",
                                            [
                                                "arn:aws:s3:::",
                                                {
                                                    "Ref": "S3BucketName"
                                                },
                                                "/*"
                                            ]
                                        ]
                                    }
                                },
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "s3:ListBucket",
                                        "s3:ListBucketMultipartUploads"
                                    ],
                                    "Resource": {
                                        "Fn::Join": [
                                            "",
                                            [
                                                "arn:aws:s3:::",
                                                {
                                                    "Ref": "S3BucketName"
                                                }
                                            ]
                                        ]
                                    }
                                }
                            ]
                        }
                    }
                ]
            }
        },
        "S3BucketUserAccessKey": {
            "Type": "AWS::IAM::AccessKey",
            "Properties": {
                "UserName": {
                    "Ref": "S3BucketUser"
                }
            }
        }
    }
}

PolicyDocumentについてs3:*のような設定で全許可することもできるようですが、今回は明示的に必要そうな権限だけを付与するような設定を行ってみました。
正直あまり自信ない(^^;

goofysでWordPressのuploadsディレクトリを共有する

s3fsより読み込みや書き込みが速い模様。goofysは今回初めての導入になります。

公式(Gitリポジトリ):GitHub – kahing/goofys: a high-performance, POSIX-ish Amazon S3 file system written in Go
参考:goofysを使ってAmazon LinuxにS3をマウントする。 – Qiita

ざっくり以下のようなことをしています。

  1. AWS::CloudFormation::Initでaws configure実行
  2. golang、goofysなどをインストール
  3. WordPressにuploadsディレクトリ作成
  4. goofysでS3バケットをマウント

当初は公式で紹介されているコマンド(go get github.com/kahing/goofys、go install github.com/kahing/goofys)でgoofysをビルドしていたのですが、CloudFormationのInit内での処理だからなのかうまく行きませんでした。ログも見てみましたがこれと言って何が原因なのかがわかりませんでした;;
調べてみたところ実行可能な状態のgoofysを直接/usr/local/bin以下に配置することもできるようになっていたのでwgetで取得したプログラムにchmodで実行権限を付与することにしました。

"LaunchConfig": {
    "Type": "AWS::AutoScaling::LaunchConfiguration",
    "Metadata": {
        "AWS::CloudFormation::Init": {
            "install_wordpress": {
                "files": {
                    "/tmp/create-wp-config": {
                        "content": {
                            "Fn::Join": [
                                "",
                                [
~~~中略~~~
                                    "# aws configure\n",
                                    "aws configure set aws_access_key_id ",
                                    {
                                        "Ref": "S3BucketUserAccessKey"
                                    },
                                    "\n",
                                    "aws configure set aws_secret_access_key ",
                                    {
                                        "Fn::GetAtt": [
                                            "S3BucketUserAccessKey",
                                            "SecretAccessKey"
                                        ]
                                    },
                                    "\n",
                                    "aws configure set region ap-northeast-1\n",
                                    "aws configure set output json\n",
                                    "\n",
                                    "# install golang and goofys\n",
                                    "yum -y install golang fuse\n",
                                    "wget https://github.com/kahing/goofys/releases/download/v0.22.0/goofys -P /usr/local/bin\n",
                                    "chmod +x /usr/local/bin/goofys\n",
                                    "\n",
                                    "# create s3 mount dir\n",
                                    "mkdir -p /var/www/html/",
                                    {
                                        "Fn::If": [
                                            "WordPressInstallDirectoryNameIsEmpty",
                                            "",
                                            {
                                                "Fn::Join": [
                                                    "",
                                                    [
                                                        {
                                                            "Ref": "WordPressInstallDirectoryName"
                                                        },
                                                        "/"
                                                    ]
                                                ]
                                            }
                                        ]
                                    },
                                    "wp-content/uploads\n",
                                    "\n",
                                    "# execute s3 mount by goofys. (48 = apache uid)\n",
                                    "export HOME=/root\n",
                                    "/usr/local/bin/goofys -o nonempty -o allow_other --uid=48 --gid=48 --dir-mode=0777 --file-mode=0777 --region=ap-northeast-1 ",
                                    {
                                        "Ref": "S3BucketName"
                                    },
                                    " /var/www/html/",
                                    {
                                        "Fn::If": [
                                            "WordPressInstallDirectoryNameIsEmpty",
                                            "",
                                            {
                                                "Fn::Join": [
                                                    "",
                                                    [
                                                        {
                                                            "Ref": "WordPressInstallDirectoryName"
                                                        },
                                                        "/"
                                                    ]
                                                ]
                                            }
                                        ]
                                    },
                                    "wp-content/uploads\n"
                                ]
                            ]
                        },
                        "mode": "000500",
                        "owner": "root",
                        "group": "root"
                    }
                }
            }
        }
    }
}

goofysによるマウントを行う際にawsコマンドでS3へのアクセスができる必要があるので事前にaws configureでデフォルト設定を作成しています。

IAMユーザーをCloudFormationで作成するようにしたため、スタック作成時に以下のようなチェック項目が表示されるようになりました。

その他、goofysのマウントコマンドでregionを指定しないとus-east-1がデフォルトとして選択されてしまったのか失敗することがありました。そのため明示的にap-northeast-1リージョンを設定しています。
ただ上記の設定を行ってもgoofysのマウントがうまく行かないことがありました。失敗したときは以下のようなログが出てました。もう少し調べる必要がありそうです。

# ↓たしかregion指定しなかったときのエラー
Nov 11 @@:@@:@@ ip-172-31-39-137 /usr/local/bin/goofys[3223]: s3.INFO Switching from region 'us-east-1' to 'ap-northeast-1'

# ↓たぶんregion指定したけどなんかダメだった時のエラー
Nov 11 @@:@@:@@ ip-172-31-32-216 /usr/local/bin/goofys[3223]: s3.ERROR code=NoCredentialProviders msg=no valid providers in chain. Deprecated.#012#011For verbose messaging see aws.Config.CredentialsChainVerboseErrors, err=<nil>#012
Nov 11 @@:@@:@@ ip-172-31-32-216 /usr/local/bin/goofys[3223]: s3.ERROR code=NoCredentialProviders msg=no valid providers in chain. Deprecated.#012#011For verbose messaging see aws.Config.CredentialsChainVerboseErrors, err=<nil>#012
Nov 11 @@:@@:@@ ip-172-31-32-216 /usr/local/bin/goofys[3223]: s3.ERROR code=NoCredentialProviders msg=no valid providers in chain. Deprecated.#012#011For verbose messaging see aws.Config.CredentialsChainVerboseErrors, err=<nil>#012
Nov 11 @@:@@:@@ ip-172-31-32-216 /usr/local/bin/goofys[3223]: main.ERROR Unable to access '[ここS3バケット名]': NoCredentialProviders: no valid providers in chain. Deprecated.#012#011For verbose messaging see aws.Config.CredentialsChainVerboseErrors
Nov 11 @@:@@:@@ ip-172-31-32-216 /usr/local/bin/goofys[3223]: main.FATAL Mounting file system: Mount: initialization failed

スティッキーセッションを有効化、設定

参考:Application Load Balancer のターゲットグループ – Elastic Load Balancing
wp-config.phpがWordPressインストール直後の状態であれば各セキュリティSALT定数の値「put your unique phrase here」という文字列が固定で入っています。複数のインスタンスで同一となるためCookieの期限が切れてDNSラウンドロビンの仕組みによって別のインスタンスにアクセスが振り分けられたとしてもログインが切れるということはないと思います。
しかし、今回のEC2の複数台構成において各SALTの値はそれぞれインスタンス立ち上げ時にランダムな値を取得して設定されるようになっています。

そのためWordPressの管理画面でログインした状態で30秒程度放置すると以下のようにセッション切れによるダイアログが表示されてしまいます。

これを維持するためにロードバランサーのターゲットグループの維持設定を有効化しました。サンプルテンプレートの時点で有効ではありましたが、維持設定の秒数が30秒と短めだったので86400秒(1日)に変更しました。

"ALBTargetGroup": {
    "Type": "AWS::ElasticLoadBalancingV2::TargetGroup",
    "Properties": {
~~~中略~~~
        "TargetGroupAttributes": [
            {
                "Key": "stickiness.enabled",
                "Value": "true"
            },
            {
                "Key": "stickiness.type",
                "Value": "lb_cookie"
            },
            {
                "Key": "stickiness.lb_cookie.duration_seconds",
-                "Value": "30"
+                "Value": "86400"
            }
        ]
    }
}

スタックの構築が失敗したときのロールバックについて

CloudFormationのスタックは起動に失敗すると自動的にロールバック処理が働いて構築中だったリソースがすべて削除されてしまいます。
失敗の原因がEC2にある場合、ロールバックが有効な状態ではインスタンスにSSHで接続してゆっくり調査を行うことができないので、検証中は無効化しておくのが良いと思います。
スタック作成画面の2ページ目か3ページ目の画面下の方に表示される「スタックの作成オプション」の中で失敗時のロールバックを無効にすることで無効化できます。

もちろん無効化した場合は起動に失敗したスタックのリソースが残り続けることになるので、不要となったタイミングでCloudFormationのスタック一覧から手動で削除する必要があります。

スタックの構築が失敗したときに確認するべきログファイルの場所とか

EC2内のコマンド操作が原因でスタック全体の構築が失敗するというケースが結構あります。
例えばsedによる置換を行う際にスラッシュが意図した通りにエスケープできていなかったりなど。
おおよそですが以下のファイルの辺り見れば何で失敗したかわかる気がしました。

  • UserDataに書いたコマンドのログ
    • /var/log/cloud-init-output.log
    • /var/log/cloud-init.log
  • AWS::CloudFormation::Init内のログ
    • /var/log/cfn-init-cmd.log
    • /var/log/cfn-init.log
  • その他、goofysによるマウントが失敗してたときのエラーログは/var/log/messageに書き込まれてたりしました。(cloud-init.logの方にsyslogを見ること!のようなニュアンスのログが残ってた気がする)

成果物について

このスタックを構築するのに使用するパラメータは以下のような感じになります。

正常にスタックが作成できると以下のような感じのイベント結果となります。
WebServerGroupで10数分待っても結果が返ってこない場合は何かしらエラー出てると思います。
S3Bucketでエラーが起きる場合は他のユーザーのバケット名とかぶったりしてると思います。

WordPressの管理画面から画像をアップロードして両方のインスタンスで画像がuploadsディレクトリで見れることを確認しました。
dfでマウントされてることも念のため確認。
その他画像の削除とかもできることを確認。

今回のテンプレートも全文はGitHubの方で公開しています。
リンク:aws-cloudformation-templates/wordpress.json at master · imo-tikuwa/aws-cloudformation-templates

その他メモ

IAMやS3バケットのPolicyDocumentがなんとーくとしかわかってない;;
S3BucketUserとS3BucketPolicyで2重に設定してるような気がする。
S3は設定を間違えると全ユーザーが参照できるバケットとか作れてしまうのでこの辺しっかり勉強しないといけないんじゃないかなーと思います。

今回、S3のバケット名をパラメータに追加しましたが、これは他のユーザーが所有してるバケット名とかを入力してしまうとエラーでスタックの作成に失敗してしまいます。
当初はバケット名は自動設定されるよう入力項目にはしてなかったのですが、作成後のバケット名がFn::GetAtt関数で取得できない模様。。(;;
公式のドキュメント:AWS::S3::Bucket – AWS CloudFormation
ググると似たようなことで悩んでる人がStackOverFlowとかで見つかったりしましたが、解決せず。。

それとここまで書いておいてS3をgoofysでマウントするという手順は正直あまり正しくないんじゃないかなと思った。どっちかというとEFS(複数台のEC2で同時に使用可能なEBSみたいなやつ)っていうサービスを使うべきなんじゃないかなーと思います。あるいはEC2にnfs構築してそれをマウントするとか。後で別途ファイル切ってプッシュするかもしれません。

あと、デフォルトで割り当てられたDNS名でのHTTPアクセスではなくRoute53で管理しているドメインをエイリアス設定してアクセス可能にしたりとか、ACMでパブリック証明書を作成してHTTPSリスナーに割り当てたりとかしてみたいと思います。この辺CloudFormationで1発で行けるのかわからないので調べる。

タイトルとURLをコピーしました