OpsWorksでカスタムックブックを作ってPHP5.6+nginxのWordPressを構築してみる

OpsWorksでPHPやってみるぞ!と思ってビルトインのPHPの構成を使ってみたらPHP5.3がインストールされたてズコーとなったので、独自でクックブックを作成してPHP5.6+nginxのWordPress環境を作成してみたのでメモ

# 参考

+ [AWS OpsWorks ハンズオン](http://www.slideshare.net/AmazonWebServicesJapan/aws-opsworks)
+ [AWS マイスターシリーズ Black belt
AWS OpsWorks](http://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-tech-aws-opsworks)
+ [AWS OpsWorksとChefを使用してWordPressを起動する方法](http://aws.aipo.com/blog/2014/09/17_104038.html)

# カスタムクックブック

作成したカスタムクックブック（ミドルウェアのインストール及びデプロイ用のレシピ）は以下にあります。

[toshihirock/opsworks](https://github.com/toshihirock/opsworks.git)


# 準備

以下のCloudFormationを使ってさくっと新しいVPC環境を作成します。
AvailabilityZoneが異なる場合は適宜変更してください。

```lang:vpc.json
{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Description" : "VPC",
  "Parameters" : {
    "VpcName" : {
      "Type" : "String"
    }
  },

  "Resources" : {
    "VPC" : {
      "Type" : "AWS::EC2::VPC",
      "Properties" : {
        "CidrBlock" : "172.30.0.0/16",
        "EnableDnsHostnames" : "true",
        "Tags" : [ {"Key": "Name", "Value": { "Ref" : "VpcName" }} ]
      }
    },

    "PublicSubnetAZa" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "AvailabilityZone" : "ap-northeast-1a",
        "CidrBlock" : "172.30.11.0/24",
        "VpcId" : { "Ref" : "VPC" }
      }
    },

    "PrivateSubnetAZa" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "AvailabilityZone" : "ap-northeast-1a",
        "CidrBlock" : "172.30.51.0/24",
        "VpcId" : { "Ref" : "VPC" }
      }
    },

    "PublicSubnetAZc" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "AvailabilityZone" : "ap-northeast-1c",
        "CidrBlock" : "172.30.12.0/24",
        "VpcId" : { "Ref" : "VPC" }
      }
    },

    "PrivateSubnetAZc" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "AvailabilityZone" : "ap-northeast-1c",
        "CidrBlock" : "172.30.52.0/24",
        "VpcId" : { "Ref" : "VPC" }
      }
    },

    "InternetGateway" : {
      "Type" : "AWS::EC2::InternetGateway"
    },

    "InternetGatewayAttach" : {
      "Type" : "AWS::EC2::VPCGatewayAttachment",
      "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "InternetGatewayId" : { "Ref" : "InternetGateway" }
      }
    },

    "DHCPOptions" : {
      "Type" : "AWS::EC2::DHCPOptions",
      "Properties" : {
        "DomainName" : "ap-northeast-1.compute.internal",
        "DomainNameServers" : [ "AmazonProvidedDNS" ]
      }
    },

    "VPCDHCPOptionsAssociation" : {
      "Type" : "AWS::EC2::VPCDHCPOptionsAssociation",
      "Properties" : {
        "VpcId" : { "Ref" : "VPC" },
        "DhcpOptionsId" : { "Ref" : "DHCPOptions" }
      }
    },

    "RouteTable" : {
      "Type" : "AWS::EC2::RouteTable",
      "Properties" : {
        "VpcId" : { "Ref" : "VPC" }
      }
    },

    "Route1" : {
      "Type" : "AWS::EC2::Route",
      "Properties" : {
        "DestinationCidrBlock" : "0.0.0.0/0",
        "RouteTableId" : { "Ref" : "RouteTable" },
        "GatewayId" : { "Ref" : "InternetGateway" }
      },
      "DependsOn" : "InternetGatewayAttach"
    },

    "SubnetRoute1" : {
      "Type" : "AWS::EC2::SubnetRouteTableAssociation",
      "Properties" : {
        "RouteTableId" : { "Ref" : "RouteTable" },
        "SubnetId" : { "Ref" : "PublicSubnetAZa" }
      }
    },

    "SubnetRoute2" : {
      "Type" : "AWS::EC2::SubnetRouteTableAssociation",
      "Properties" : {
        "RouteTableId" : { "Ref" : "RouteTable" },
        "SubnetId" : { "Ref" : "PublicSubnetAZc" }
      }
    },

    "SecurityGroupDefault" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "Default Security Group",
        "VpcId" : { "Ref" : "VPC" }
      }
    },

    "ingress" : {
      "Type" : "AWS::EC2::SecurityGroupIngress",
      "Properties" : {
        "GroupId" : { "Ref" : "SecurityGroupDefault" },
        "IpProtocol" : "tcp",
        "ToPort" : 22,
        "FromPort" : 22,
        "CidrIp" : "0.0.0.0/0"
      }
    },

    "egress" : {
      "Type" : "AWS::EC2::SecurityGroupEgress",
      "Properties" : {
        "GroupId" : { "Ref" : "SecurityGroupDefault" },
        "IpProtocol" : "-1",
        "CidrIp" : "0.0.0.0/0"
      }
    }
  },

  "Outputs" : {
    "PublicSubnetAZa" : {
      "Value" : { "Ref" : "PublicSubnetAZa" } 
    },
    "SecurityGroupDefault" : {
      "Value" : { "Ref" : "SecurityGroupDefault" } 
    }
  }
}


```

以下で実行します。

```
$aws cloudformation create-stack --stack-name ops-works --template-body file://vpc.json --parameters ParameterKey=VpcName,ParameterValue=ops-works
```

# RDSの作成

WordPressではデータベースを利用するのでRDSを利用してMySQLを作成します。

最初にRDSのSubnetGroupsを作成します。
先ほどCloudFormationで作成したVPCのサブネットを選択して作成します。

次に以下のような内容でRDSを作成します。

+ DB Engine->MySQL
+ SecurityGroup->3306ポートでinboundの通信ができるもの
+ VPC->先ほど作成したVPC
+ Subnet->PrivateSubnet(10.1.15.0/24)


## スタックの作成

OpsWorksのトップエンティティとなるスタックを最初に作成します。
今回はカスタムクックブックを利用するのでレシピが格納されたGitHubのURLを指定しています。

![Screen Shot 2015-09-22 at 10.28.24 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/165126e8-b631-af0a-dfe1-adad61fbd02b.png "Screen Shot 2015-09-22 at 10.28.24 PM.png")


![Screen Shot 2015-09-22 at 7.21.14 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/c91fa30f-2a43-901d-7ed0-db92854d2cc5.png "Screen Shot 2015-09-22 at 7.21.14 PM.png")


+ vpc->作成したVPCを指定
+ DefaultSubnet->PublicSubnet(172.30.11.0/24)
+ OS->AmazonLinux2015.03
+ IAMrole->aws-opsworks-service-role
+ SSH Key->存在するSSHキー
+ IAM instance profile->aws-opsworks-ec2-role
+ Use custome Chef cookbooks->Yes
+ Repository type->Git
+ RepositoryURL->https://github.com/toshihirock/opsworks.git

# レイヤーの追加

次にレイヤーというスタックに紐づく機能ごとにインスタンスをまとめたものを定義します。Layer TypeというものでJavaやRailsなど指定すると対象のWebサーバーやアプリケーションがOpsWorksによってよってインストールされます。今回はカスタムクックブックを利用したいので必要最低限のレシピのみ実行されるCustomTypeを選びます。(規定レイヤーにも任意のレシピの追加は可能ですが）

設定は以下のようにします。

+ Layer type->Custom
+ Name->PHP56AppServer(任意)
+ short name->php56(任意)

![Screen Shot 2015-09-22 at 7.16.48 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/b1cf6b3b-2716-1378-ac12-4c791f29764a.png "Screen Shot 2015-09-22 at 7.16.48 PM.png")


このままだとインターネットとアクセスできず、OpsWorksAgentが動作しないのでPublic IP addressesを有効にします。

![Screen Shot 2015-09-22 at 7.15.16 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/a6b042cf-39d7-2637-1ff0-9d7fb14fa0f5.png "Screen Shot 2015-09-22 at 7.15.16 PM.png")

# インスタンスの追加

レイヤーを追加しただけではインスタンスは起動しないため、先ほど作成したレイヤーに紐づくインスタンスを起動します。

![Screen Shot 2015-09-22 at 10.44.00 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/8386a3bf-8c28-a4bd-09e0-eecc7f41004d.png "Screen Shot 2015-09-22 at 10.44.00 PM.png")


Customレイヤーでは最低限のレシピのセットアップのみなのでt2.microでも起動が可能です。SubnetはPublicSubnetを指定してください。

上記画面でAdd Instanceをボタンを押します。
その後、遷移する画面でstartさせます。(Add Instanceボタンを押しただけでは起動しない）online状態になるまで5分ほど掛かります。

# RDSレイヤーの追加

先ほど作成したRDSをRDS用のレイヤーとして登録します。レイヤーとして登録しておくことデプロイの処理でDBのホスト名やテーブル、パスワードなどの情報を参照できるようになります。

![Screen Shot 2015-09-22 at 10.38.38 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/a070cfa1-d12c-e16e-f0a2-6dcdd12b3e56.png "Screen Shot 2015-09-22 at 10.38.38 PM.png")

# 起動したインスタンスにレシピを実行し、nginx+php5.6+php-fpmをインストールする

起動したインスタンスにはWebサーバーやPHPなど入っていないのでミドルウェアをインストールするためのレシピを手動で実行します。

まず、Stack->WordPressStackと選択し、Run Commandを選択します。

![Screen Shot 2015-09-23 at 1.54.10 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/45463b92-dc3c-27b8-8737-2ab36cf399fe.png "Screen Shot 2015-09-23 at 1.54.10 PM.png")

次に **Update Custom Cookbooks** を選択し、実行します。本実行により、GitHubに登録されたレシピを対象レイヤーのインスタンス群にcloneします。

![Screen Shot 2015-09-23 at 1.58.25 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/cde95819-94cb-fffe-3c3a-af1fde41f4c5.png "Screen Shot 2015-09-23 at 1.58.25 PM.png")

無事成功すれば以下のような画面が表示され、/opt/aws/opsworks/current/site-cookbooks配下に独自のレシピが配置されています。sshログインすれば確認も可能です。

![mosaiku.png](https://qiita-image-store.s3.amazonaws.com/0/20193/32b9b675-9042-ea2f-439d-e17149afaaa3.png "mosaiku.png")

次にカスタムクックブックのレシピの実行を行います。
上記画面の **Repeat** を選択し画面を遷移します。
次の画面（下記画面）のCommandで **Execute Recipes** を選択して任意のレシピを実行します。GitHubにコードはありますが、今回はmywebというクックブックのdefaultというレシピでインストールや設定を行うので **myweb::default** と指定し、実行します。

![Screen Shot 2015-09-23 at 2.17.20 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/21679862-5469-8f91-6bea-72b65e810e6e.png "Screen Shot 2015-09-23 at 2.17.20 PM.png")

完了後、http://{publicIP}/phpinfo.php でアクセスするとPHPinfoの情報が確認でき、最新のPHP5.6.13とphp-fpmが利用されていることがわかります。また、sshログインするとWebサーバーもnginxの1.8が利用されていることが分かります。

![Screen Shot 2015-09-23 at 2.23.39 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/a75e48c0-5949-597e-f469-d683c95e28c8.png "Screen Shot 2015-09-23 at 2.23.39 PM.png")

# WordPressをAppとして登録する

OpsWorksではAppといってデプロイするアプリケーションを管理する機能があるため、この機能を利用して最新のWordPressをインスタンスに配置します。WordPressは最新のものを利用するためにWordPressのソースコードがホスティングされているGitHubのURLを指定しています。

![Screen Shot 2015-09-23 at 11.05.46 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/8da0da94-8c83-84d9-b724-6c28500ec069.png "Screen Shot 2015-09-23 at 11.05.46 PM.png")

![Screen Shot 2015-09-23 at 11.05.56 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/f4602f06-2ec8-07cb-33bb-86b11f03326f.png "Screen Shot 2015-09-23 at 11.05.56 PM.png")

+ Name->WordPress
+ Type->PHP
+ Data source type->RDS
+ Data instance->先ほどレイヤーとして登録したDBインスタンス
+ Database name->wordpress（任意。デプロイ時にこのデータベースを作成する)
+ Repository type->Git
+ Repository URL->https://github.com/WordPress/WordPress.git

# アプリをデプロイしてみる

これでアプリをデプロイできる状態ができた!と思っていたのですが、CustomLayerの場合にはデプロイ時に登録したアプリをインスタンスに配置するようなレシピはないので、自作する必要が有ります。ビルドインのPHP Layerを指定した場合とCustom Layerを指定した場合のデフォルトで設定されているレシピの違いは以下の通りです。

![Screen Shot 2015-09-22 at 3.56.00 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/8a2d5256-3c5a-f1ca-aefb-cc6c2c093e3c.png "Screen Shot 2015-09-22 at 3.56.00 PM.png")

![Screen Shot 2015-09-22 at 3.51.29 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/e7b6889b-553c-3eb8-057a-2d70d3227567.png "Screen Shot 2015-09-22 at 3.51.29 PM.png")

デプロイを実行した場合、どちらのレイヤーでもdeploy::defaultレシピは実行されますが、Custom layerではdeploy::phpが実行されないのでアプリの配置やWebサーバーの再起動など実施されません。

Custom Layerでもdeploy::phpを実行すればよいのかというという気もしますが、いくつか問題があります。実際にdeploy::phpレシピの処理を見るとわかりますが、以下の問題があります。

+ mod_php5_apache2とmod_php5_apache2::phpという今回は不要なレシピのinludeを実施している
+ MySQLにwordpress用のデータベースがなければ作成する処理がない
+ WordPressアプリケーションがdeployユーザー、apacheグループで配置されるため、wp-config.phpがnginxで作成できない

とはいっても1からから色々作成するのは面倒なので既存のdeploy::phpを参考に最小限の記述だけします。なお、deploy::phpなど既存のすべてのレシピはGitHubで見ることができます。

[aws/opsworks-cookbooks](https://github.com/aws/opsworks-cookbooks)


最終的に以下のようにカスタムクックブックにdeploy/attributes/customize.rbとdeploy/recipes/webapp_deploy.rbを作成して問題を解消しています。

```lang:customize.rb
###
# This is the place to override the deploy cookbook's default attributes.
#
# Do not edit THIS file directly. Instead, create
# "deploy/attributes/customize.rb" in your cookbook repository and
# put the overrides in YOUR customize.rb file.
###

# The following shows how to override the deploy user and shell:
#
normal[:opsworks][:deploy_user][:group] = 'nginx'
normal[:opsworks][:deploy_user][:user] = 'nginx'
```

customize.rbは既存のdeploy/attributes/customize.rbをコピーして変更したい値のみ記述しています。上記を記載することで既存のattributeの値を上書きし、デプロイする時のユーザーとグループをそれぞれnginxとなるように設定しています。

デプロイするためのレシピがmyweb_php.rbになります。

```lang:myweb_php.rb
#
# Cookbook Name:: deploy
# Recipe:: php
#

include_recipe 'deploy'

dbname = node[:deploy][:wordpress][:database][:database]
dbuser = node[:deploy][:wordpress][:database][:username]
dbpass = node[:deploy][:wordpress][:database][:password]
dbhost = node[:deploy][:wordpress][:database][:host]

mysql_command = "/usr/bin/mysql -u #{dbuser} -p#{dbpass} -h #{dbhost}"

#include_recipe "mod_php5_apache2"
#include_recipe "mod_php5_apache2::php"

node[:deploy].each do |application, deploy|
  if deploy[:application_type] != 'php'
    Chef::Log.debug("Skipping deploy::php application #{application} as it is not an PHP app")
    next
  end

  execute "create mysql databases" do
    command  "#{mysql_command} -e 'CREATE DATABASE #{dbname}'"
    not_if do
     system("#{mysql_command} -e 'SHOW DATABASES' | egrep -e '^#{dbname}$'") 
    end
  end

  opsworks_deploy_dir do
    user deploy[:user]
    group deploy[:group]
    path deploy[:deploy_to]
  end

  opsworks_deploy do
    deploy_data deploy
    app application
  end

  service 'nginx' do
    action :restart
  end
end
```

こちらもは既存のdeploy/recipes/php.rbをコピーして変更しています。opsworks_deploy_dirとopsworks_deployの処理は既存のdeploy/definitions配下に定義してあるものをそのまま利用しています。（この処理でソースコードのcloneやDocumentRootへの配備などを実施）また、RDSをDataSourceとして指定しているのでデータベースホストやデータベース名をレシピで取得し、初回のみMySQLに指定されたデータベースを作成する処理を追加しています。

レシピができたら上記のレシピをデプロイ時に実行されせるようにします。

PHP56AppServerレイヤーのRecipesを選択し、Editを選択することでカスタムクックブックを任意のライフサイクルのタイミングで実行できるように設定できます。（実行タイミングは既存のクックブック終了後）設定画面は以下のようになっています。

![Screen Shot 2015-09-23 at 11.37.14 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/fd2ced1e-ad33-a655-88e8-72bd039250f4.png "Screen Shot 2015-09-23 at 11.37.14 PM.png")

上記完了後、手動でデプロイを実行します。

下記Apps画面を表示すると登録したアプリ一覧が表示され、deployボタンがあるのでこちらを選択します。

![Screen Shot 2015-09-23 at 11.40.59 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/00e6de47-6e27-2313-8c51-025ce614c84e.png "Screen Shot 2015-09-23 at 11.40.59 PM.png")

deploy選択後、以下の画面でDeployを選択します。

![Screen Shot 2015-09-23 at 11.49.47 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/2d65643b-44ea-7ddf-fb9b-f5d31fcf028d.png "Screen Shot 2015-09-23 at 11.49.47 PM.png")


完了後、http://{publicIP}/wordpress/current/にアクセスし、以下のような画面が表示されればOKです。

![Screen Shot 2015-09-23 at 11.57.20 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/d72ef975-3b96-5714-39c8-fe7bcce64813.png "Screen Shot 2015-09-23 at 11.57.20 PM.png")

そのまま言語を選択し、DBの設定を行えばWordPressが利用できます。

![Screen Shot 2015-09-24 at 12.00.03 AM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/150fbddf-54b5-0147-d756-bc3abdf5236b.png "Screen Shot 2015-09-24 at 12.00.03 AM.png")


# レイヤーにインスタンス追加時に自動でミドルウェアの設定をできるようにする

レイヤーにインスタンスを追加した場合、Setupイベント->deployイベント->Configureイベントが実行され、対応するレシピ群が実行されます。先ほどPHP5.6やミドルウェアのインストールは手動で行ったので、Setupイベントで上記を実行するようにして、インスタンス追加時にミドルウェアの設定が自動でできるようにします。

レイヤーを選択してdeployイベントにカスタムクックブックを追加した時と同じようにmyweb::defaultを設定します。

![Screen Shot 2015-09-23 at 11.47.03 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/40750e5f-d0e2-b009-13fd-501c3567c732.png "Screen Shot 2015-09-23 at 11.47.03 PM.png")

この状態でレイヤーにインスタンスを追加して先ほどと同じように起動したインスタンスにアクセスするとWordPressの画面が表示されるかと思います。(WordPressのデプロイは先ほどdeploy時にカスタムクックブックを実行するように設定したのでそれが実行され、配置される）

# まとめ

+ 初めてOpsWorksをがっつり使ってみたが、既存構成を変えるのは思ったよりも大変。特にデプロイ周り
+ 実際は上記ではまだ足りない部分があるので(Undeployのレシピなど)もあるので実際はもう少し大変そう
+ 追加のパッケージやパッケージのバージョン変更などの場合、customize.rbを利用して設定を上書きするようなやり方のほうがよさそう（このやり方をそもそも初めて知った。これでattributeを気軽に変更できるのは良い)
+ OpsWorksでデフォルトでビルドインされているレシピのコードは見ておいたほうが良い
+ とはいえ一度レシピを作ってしまうとインスタンスの追加やデプロイもすごい楽でその辺りは魅力的