はじめに
WordPressでサイトを公開していて、以下のような課題に悩まされた経験はありませんか?
- 脆弱性を狙った攻撃の対象になる セキュリティ の課題
- 負荷対策が必要となる パフォーマンスとスケーラビリティ の課題
- DBが落ちて
データベース接続確立エラー
となったり EC2のリタイアメント等の必須メンテナンスの発生による 運用 の課題 - サーバ費用等の コスト の課題
このような課題をシンプルに解決するため、コーポレートサイトのリニューアルを機に、WordPressを静的サイト化してみました。
全体像
この構成がもたらす課題解決
- セキュリティ
- OriginがEC2上のWordPressではなくS3上の静的HTMLとなる為、攻撃可能領域がほとんど無くなります
- パフォーマンスとスケーラビリティ
- CDN(CloudFront)を噛ませる事で、スケーラビリティとパフォーマンスの課題を解決します
- 運用
- slack経由で簡単にEC2の起動/停止とデプロイが可能となります
- デザイナーやバックオフィス系の方々(広報/人事等)が、ご自身でサーバ起動停止やデプロイが出来るようになりました
- WordPress本体(EC2)が公開サイトから切り離されている為、いつでも好きなタイミングでメンテナンスが可能となります
- slack経由で簡単にEC2の起動/停止とデプロイが可能となります
- コスト
- 常時発生するコストはCloudFrontとS3のみとなり大幅なコスト削減が可能となります
この構成のポイント
- WordPress(EC2)
- slack経由でEC2の起動/停止とデプロイを指示
- EC2はサイト更新時にのみ起動すればよい
- WordPress内のコンテンツを更新しても閲覧サイトへは反映されない(静的HTML出力→S3デプロイ後に閲覧可能となる
- 信頼する接続元IP(管理者のOffice, VPN等)からのみアクセス可能となるようSecurityGroupでIP制限を行う
- EC2の停止忘れ防止の為、夜間自動的に停止する処理を導入
- slack経由でEC2の起動/停止とデプロイを指示
- 閲覧用サイト(CloudFront+S3)
- キャッシュ期間は長めでOK(デプロイ時にキャッシュ削除(Invalidation)を実施
- S3+CloudFrontの構成で静的Webサイトを構築した場合、サブディレクトリからデフォルトのルートオブジェクトを返さないという問題をCloudFront Functionsで解決
- AWS Chatbot
- slack→Lambda→SSM(Run Command)経由でEC2上のデプロイスクリプトを実行
- EC2の起動停止はslack→AWS Chatbotからaws cli経由で直接実行
- 余談
- EC2に直接S3をマウントする方法(goofys, s3fs等)も検討しましたが、動作が不安定だったため不採用としました。
構築手順
1. WordPress(EC2)の構築
EC2上に、WordPressを構築しWebサイトを作成します
- EC2上にWordpressをインストールします。詳細な手順は割愛しますが下記の記事が参考になります
- WordPressを使ってWebコンテンツを作成します
2. 閲覧用サイト(CloudFront+S3)の構築
閲覧用サイトを構築します
- CloudFront+S3で静的Webサイトを構築します。詳細な手順は割愛しますが下記の記事が参考になります
3. デプロイ処理の構築
EC2上の静的HTMLをS3に同期し、CloudFrontのキャッシュ削除(Invalidation)を行う処理を作ります
- WordPressに静的化プラグインを導入します
- Simply Static等のプラグインを導入し、WordPress上のWebコンテンツを静的HTMLとしてローカルに出力できる事を確認します
- デプロイに必要な権限を付与したEC2用のIAM Roleを作成し、EC2(WordPress)にアタッチします
IAM Role
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"cloudfront:CreateInvalidation",
"cloudfront:GetDistribution",
"cloudfront:GetDistributionConfig",
"cloudfront:GetInvalidation",
"cloudfront:ListDistributions",
"cloudfront:ListInvalidations",
"cloudfront:ListStreamingDistributions"
],
"Resource": [
"arn:aws:cloudfront::123456789012:distribution/ディストリビューションID"
]
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::s3-bucket-name/*",
"arn:aws:s3:::s3-bucket-name"
]
},
{
"Effect": "Allow",
"Action": "s3:ListAllMyBuckets",
"Resource": "arn:aws:s3:::*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetEncryptionConfiguration"
],
"Resource": "*"
}
]
}
- デプロイ用スクリプトを作成し、動作確認します
shellの実装例(/usr/local/bin/s3_sync.sh)
#!/bin/sh
#set -e
DISTRIBUTION_ID=ディストリビューションID
ORIGIN_PATH=静的HTML出力先DIRのパス
DISTINATION_PATH=s3://s3-bucket-name/
INVALIDATION_PATHS="/*"
echo "----- s3 sync ----"
set -x
aws s3 sync ${ORIGIN_PATH} ${DISTINATION_PATH} --delete
set +x
echo ""
echo "----- create invalidation ----"
set -x
aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION_ID" \
--paths "$INVALIDATION_PATHS"
4. Lambdaの設定
LambdaからSSM経由でEC2上のデプロイ用スクリプトを実行させます
- EC2(WordPress)に割り当てたIAM RoleにSSM関連の許可ポリシーを追加でアタッチします
- Lambda用のIAM Roleを新規作成し、以下のポリシーをアタッチします
- slackのweb hook用URLを発行しておきます(手順は割愛
- Lambda関数を作成します
-
一から作成
を選択し、上で作成したIAM Roleを実行ロールとして割り当て関数を作成します- (詳細設定はチェック不要)
-
- コードを実装します
Python3.9の実装例(lambda_function.py)
import boto3
import logging
import requests
import time
import json
logger = logging.getLogger()
logger.setLevel(logging.INFO)
ec2 = boto3.client('ec2')
ssm = boto3.client('ssm')
def lambda_handler(event, context):
try:
## 固定値(インスタンス再作成時は更新すること)
instanceId = "i-*****************"
r = ssm.send_command(
InstanceIds = [instanceId],
DocumentName = "AWS-RunShellScript",
Parameters = {
"commands": [
"sh /usr/local/bin/s3_sync.sh"
],
"executionTimeout": ["3600"]
},
)
command_id = r['Command']['CommandId']
## Command結果待ちループ
while True:
time.sleep(3)
res = ssm.list_command_invocations(
CommandId = command_id,
Details = True
)
invocations = res['CommandInvocations']
if len(invocations) <= 0:
continue
status = invocations[0]['Status']
if status in {'Success','Failed'}:
break
## 結果格納
dict = {
'Success':{
'color':'good',
'pretext':'Result:成功 :white_check_mark: :thumbsup:'},
'Failed':{
'color':'danger',
'pretext':'Result:失敗 :warning: :thumbsdown:'}
}
response = invocations[0]['CommandPlugins'][0]['Output']
logger.info('## 実行結果 Status:[' + status + ']\r' + response)
## slackにPost
WEB_HOOK_URL = "https://hooks.slack.com/services/XXXXXXX/YYYYYYY/abcdefg1234567890"
requests.post(WEB_HOOK_URL, headers={'Content-Type': 'application/json'}, data=json.dumps({
'text': dict[status]['pretext'] + '```' + response + '```'
}))
except Exception as e:
logger.error(e)
raise e
- コードをzipで固めてLambda関数にデプロイします
5. AWS Chatbotの設定
AWS Chatbotをslackと連携し、slackからLambdaを実行したりEC2を起動停止したり出来るようにします
- AWS Chatbot とSlackを連携します
- 下記サイトを参考に設定を行います(Lambda関数は既に作成済みのため
Lambda関数作成
の章は飛ばしてOK
- 下記サイトを参考に設定を行います(Lambda関数は既に作成済みのため
- slackから
@aws lambda invoke --function-name Lambda関数名 --region ap-northeast-1
のようにしてLambdaを実行 (途中[Run] command
ボタンをクリックする必要がある)し、下記のような実行結果が表示されればOKです
6. その他、細かい設定
- slackからEC2を起動停止できるようにする
- Chatbot用IAM Roleに以下のようなインラインポリシーを追加(WordPressのインスタンスIDのみ起動/停止/確認を許可)し、 slackから
@aws ec2 {start|stop}-instances --instance-ids i-*************** --region ap-northeast-1
のようにして実行
- Chatbot用IAM Roleに以下のようなインラインポリシーを追加(WordPressのインスタンスIDのみ起動/停止/確認を許可)し、 slackから
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:StartInstances",
"ec2:StopInstances"
],
"Resource": "arn:aws:ec2:*:*:instance/i-***************"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeInstanceStatus"
],
"Resource": "*"
}
]
}
- S3+CloudFrontの構成で静的Webサイトを構築した場合に、サブディレクトリからデフォルトのルートオブジェクトを返さないという問題の解決
- 過去に別記事としてまとめましたので参考までに
以上!
あとがき
いかがでしたでしょうか?
リプレース前は、WordPressの前にCloudFrontやWAFを置いて負荷耐性やセキュリティ対策をしていたのですが、EC2のメンテナンスがやり辛い点、たまにプロセスが落ちたり固まってしまう問題等、運用面で面倒な課題がありましたが、現在の構成にすることで様々な課題が解決され、低コストで楽に運用ができるようになりました。
同じような課題に悩まされている方の参考になれば幸いです。