AWS CLIでCode4兄弟によるEC2+nginx+Laravelの継続的デプロイ環境を構築する

これはパーソルキャリア Advent Calendar 2018の4日目です。

AWS CLIを使ってEC2+nginx+Laravelな環境をCode4兄弟(CodeCommit, CodeBuild, CodeDeploy, CodePipeline)で継続的デプロイできるようにする手順をまとめてみました。

構築する環境

AWS CLIMacで実行し必要の都度、EC2にSSHして進めます。

Mac環境は以下です。

EC2環境は以下です。

まずEC2+nginx+Laravelの環境を構築します。そしてCode4兄弟を導入して継続的デプロイできる構成にしていきます。 ここでの継続的デプロイとは、CodeCommit/Laravelプロジェクトのmasterブランチに変更をpushしたら自動でEC2にデプロイされる仕組みのことです。

最終的にCode4兄弟の役割は下記のようにします。

  • CodeCommit : LaravelプロジェクトのGitリポジトリを管理する
  • CodeBuild : composer installを実施してvendorディレクトリを生成する
  • CodeDeploy : EC2インスタンスにLaravelプロジェクト設置しnginxに反映する
  • CodePipeline : CodeCommit, CodeBuild, CodeDeployを順に実施する

最終的な構成は下記です。

f:id:nihma:20181202183343p:plain

構築の手順

今回はMac環境に作業ディレクトリを作ってここで作業します。

$ mkdir work
$ cd work

最初のEC2+nginx+Laravelな環境を作成する

まずは前提となるVPC環境を作ります。

$ vpc=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16)
$ vpc_id=$(echo $vpc | jq -r ".Vpc.VpcId")
$ aws ec2 create-tags \
    --resources ${vpc_id} \
    --tags Key=Name,Value=test-laravel

インターネットゲートウェイVPCに紐付けます。

$ internet_gateway=$(aws ec2 create-internet-gateway)
$ internet_gateway_id=$( \
    echo $internet_gateway | jq -r ".InternetGateway.InternetGatewayId")
$ aws ec2 create-tags \
    --resources ${internet_gateway_id} \
    --tags Key=Name,Value=test-laravel
$ aws ec2 attach-internet-gateway \
    --internet-gateway-id ${internet_gateway_id} \
    --vpc-id ${vpc_id}

VPCにサブネットを作成します。

$ subnet=$( \
    aws ec2 create-subnet \
        --vpc-id ${vpc_id} \
        --cidr-block 10.0.0.0/24 \
        --availability-zone ap-northeast-1a)
$ subnet_id=$(echo $subnet | jq -r ".Subnet.SubnetId")
$ aws ec2 create-tags \
    --resources ${subnet_id} \
    --tags Key=Name,Value=test-laravel

ルートテーブルにインターネットゲートウェイのルーティング設定を追加します。

$ route_table_id=$( \
    aws ec2 describe-route-tables \
        --filters Name=vpc-id,Values=${vpc_id} \
             | jq -r ".RouteTables[].RouteTableId")
$ aws ec2 create-tags \
    --resources ${route_table_id} \
    --tags Key=Name,Value=test-laravel
$ aws ec2 create-route \
    --route-table-id ${route_table_id} \
    --destination-cidr-block 0.0.0.0/0 \
    --gateway-id ${internet_gateway_id}
$ aws ec2 associate-route-table \
    --route-table-id ${route_table_id} \
    --subnet-id ${subnet_id}

EC2用にSSH(22)とHTTP(80)を許可したセキュリティグループを作成します。

$ security_group=$( \
    aws ec2 create-security-group \
        --group-name test-laravel \
        --vpc-id ${vpc_id} \
        --description "test laravel")
$ security_group_id=$(echo $security_group | jq -r ".GroupId")
$ aws ec2 authorize-security-group-ingress \
    --group-id ${security_group_id} \
    --protocol tcp \
    --port 22 \
    --cidr 0.0.0.0/0
$ aws ec2 authorize-security-group-ingress \
    --group-id ${security_group_id} \
    --protocol tcp \
    --port 80 \
    --cidr 0.0.0.0/0

EC2インスタンスへのSSHアクセスの為にキーペア/pemを作成します。

$ key_pair=$(aws ec2 create-key-pair --key-name test-laravel)
$ echo $key_pair | jq -r ".KeyMaterial" > test-laravel.pem
$ chmod 400 test-laravel.pem

EC2インスタンスを作成します。使うAMIはAmazon Linux 2 AMI 2.0.20181114 x86_64 HVM gp2です。

$ aws ec2 describe-images \
    --image-ids ami-0a2de1c3b415889d2 \
    --region ap-northeast-1
{
    "Images": [
        {
            "ImageLocation": "amazon/amzn2-ami-hvm-2.0.20181114-x86_64-gp2",
            "ImageId": "ami-0a2de1c3b415889d2",
            "SriovNetSupport": "simple",
            "ImageType": "machine",
            "CreationDate": "2018-11-14T09:06:55.000Z",
            "RootDeviceType": "ebs",
            "VirtualizationType": "hvm",
            "Architecture": "x86_64",
            "ImageOwnerAlias": "amazon",
            "State": "available",
            "Hypervisor": "xen",
            "Description": "Amazon Linux 2 AMI 2.0.20181114 x86_64 HVM gp2",
            "RootDeviceName": "/dev/xvda",
            "Name": "amzn2-ami-hvm-2.0.20181114-x86_64-gp2",
            "OwnerId": "137112412989",
            "Public": true,
            "EnaSupport": true,
            "BlockDeviceMappings": [
                {
                    "DeviceName": "/dev/xvda",
                    "Ebs": {
                        "SnapshotId": "snap-0d789818b1297bb21",
                        "Encrypted": false,
                        "VolumeSize": 8,
                        "DeleteOnTermination": true,
                        "VolumeType": "gp2"
                    }
                }
            ]
        }
    ]
}
$ ec2=$(aws ec2 run-instances \
    --image-id ami-0a2de1c3b415889d2 \
    --key-name test-laravel \
    --security-group-ids ${security_group_id} \
    --subnet-id ${subnet_id} \
    --associate-public-ip-address \
    --instance-type t2.micro)
$ ec2_instance_id=$(echo $ec2 | jq -r ".Instances[].InstanceId")
$ aws ec2 create-tags \
    --resources ${ec2_instance_id} \
    --tags Key=Name,Value=test-laravel

EC2インスタンスにElastic IPを割り当てます。

$ elastic_ip=$(aws ec2 allocate-address)
$ ec2_ip=$(echo $elastic_ip | jq -r ".PublicIp")
$ elastic_ip_id=$(echo $elastic_ip | jq -r ".AllocationId")
$ aws ec2 associate-address \
    --allocation-id ${elastic_ip_id} \
    --instance ${ec2_instance_id}

これでEC2インスタンスSSHできるようになりました 🎉

$ ssh -i test-laravel.pem ec2-user@${ec2_ip}

続いてEC2インスタンスにnginx+Laravel環境を作ります。

まずは必要なnginxとphp-fpmをインストールします。

$ sudo amazon-linux-extras install nginx1.12 -y
$ sudo amazon-linux-extras install php7.2 -y

php-fpmのuserとgroupをnginxに変更します。

$ sudo yum install patch -y
$ cat www.conf.patch
--- www.conf
+++ www.conf
@@ -21,9 +21,9 @@
 ; Note: The user is mandatory. If the group is not set, the default user's group
 ;       will be used.
 ; RPM: apache user chosen to provide access to the same directories as httpd
-user = apache
+user = nginx
 ; RPM: Keep a group allowed to write in log dir.
-group = apache
+group = nginx

 ; The address on which to accept FastCGI requests.
 ; Valid syntaxes are:
$ sudo patch -u /etc/php-fpm.d/www.conf < www.conf.patch

ComposerとLaraveiに必要なPHPパッケージをインストールします。

$ curl -sS https://getcomposer.org/installer | sudo php
$ sudo cp composer.phar /usr/local/bin/composer
$ sudo ln -s /usr/local/bin/composer /usr/bin/composer

$ sudo yum install -y php-mbstring php-xml php-bcmath

Laraveiプロジェクトを作成します。

$ cd /usr/share/nginx/html/
$ sudo composer create-project --prefer-dist "laravel/laravel=5.7.*" test-laravel
$ sudo chown -R nginx ./test-laravel*
$ cd ~/

nginxのルートディレクトリをLaraveiプロジェクトに変更します。

$ cat nginx.conf.patch
--- nginx.conf
+++ nginx.conf
@@ -39,7 +39,7 @@
         listen       80 default_server;
         listen       [::]:80 default_server;
         server_name  _;
-        root         /usr/share/nginx/html;
+        root         /usr/share/nginx/html/test-laravel/public;

         # Load configuration files for the default server block.
         include /etc/nginx/default.d/*.conf;
$ sudo patch -u /etc/nginx/nginx.conf < nginx.conf.patch

nginxとphp-fpmを起動するとブラウザ(http://${ec2_ip})からアクセスできるようになります🎉

$ sudo systemctl start nginx.service
$ sudo systemctl start php-fpm.service

LaravelプロジェクトをCodeCommit管理にする

EC2で作成したLaravelプロジェクトをアーカイブします。

$ cd /usr/share/nginx/html
$ sudo tar cvfz test-laravel.tar.gz test-laravel
$ cd -
$ exit

Laravelプロジェクトのアーカイブを取得・展開してGitリポジトリを作成します。

$ scp -i test-laravel.pem -P 22 ec2-user@${ec2_ip}:/usr/share/nginx/html/test-laravel.tar.gz ./
$ tar xvfz test-laravel.tar.gz
$ cd test-laravel
$ git init
$ git add -A
$ git commit -m "Initial commit"

CodeCommitプロジェクトを作成しLaravelプロジェクトをpushします。

$ codecommit=$(aws codecommit create-repository --repository-name test-laravel)
$ codecommit_arn=$(echo $codecommit | jq -r ".repositoryMetadata.Arn")
$ git remote add origin ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/test-laravel
$ git push origin master
$ cd ..

EC2インスタンスのLaravelプロジェクトのディレクトリ構成を自動デプロイ向けに変更する

下記のようにreleasesへタイムスタンプをディレクトリ名としてLaravelプロジェクトをデプロイします。そしてreleaseのシンボリック先を変更して必要な処理を行います。最後にcurrentが有効なプロジェクトへのシンボリックリンクになるように切り替えを行う方式にします。

/user/share/nginx/html/test-laravel
.
├── .env
├── current -> /user/share/nginx/html/laravel/releases/1533870527 ← ルートディレクトリ
├── release -> /user/share/nginx/html/laravel/releases/1533870527 ← デプロイ作業中のみできる
├── releases
│   └── 1533870527 ← タイムスタンプ
│       ├── .env -> /user/share/nginx/html/laravel/.env
│       ├── strage -> /user/share/nginx/html/laravel/shared/strage
│       └── その他のtest-laravelコード
└── shared
    └──strage

まず、nginxとphp-fpmを停止します。

$ ssh -i test-laravel.pem ec2-user@${ec2_ip}

$ sudo systemctl stop nginx.service
$ sudo systemctl stop php-fpm.service

ディレクトリ構成を変更します。

$ cd /usr/share/nginx/html/

$ timestamp=$(date +%s)

$ sudo mv test-laravel ${timestamp}
$ sudo mkdir -p test-laravel
$ cd test-laravel

$ sudo mkdir -p ./shared
$ sudo mkdir -p ./releases
$ sudo mv ../${timestamp} ./releases/

$ deploy_path="/usr/share/nginx/html/test-laravel"

$ sudo ln -nfs "${deploy_path}/releases/${timestamp}" ./release
$ sudo mv ./release/storage ./shared/
$ sudo mv ./release/.env ./

$ cd ./release
$ sudo ln -nfs ${deploy_path}/.env ./.env
$ sudo ln -nfs ${deploy_path}/shared/storage ./storage

$ cd ../

$ sudo ln -nfs "${deploy_path}/releases/${timestamp}" ./current
$ sudo unlink ./release

$ cd ../

$ sudo chown -R nginx ./test-laravel*

$ cd ~/

nginxのルートディレクトリを新しいディレクトリ構成に変更します。

$ cat nginx.conf.2.patch
--- nginx.conf
+++ nginx.conf
@@ -39,7 +39,7 @@
         listen       80 default_server;
         listen       [::]:80 default_server;
         server_name  _;
-        root         /usr/share/nginx/html/test-laravel/public;
+        root         /usr/share/nginx/html/test-laravel/current/public;

         # Load configuration files for the default server block.
         include /etc/nginx/default.d/*.conf;
$ sudo patch -u /etc/nginx/nginx.conf < nginx.conf.2.patch

nginxとphp-fpmを起動すると新しいディレクトリ構成でブラウザ(http://${ec2_ip})からアクセスできるようになります🎉

$ sudo systemctl start nginx.service
$ sudo systemctl start php-fpm.service

$ exit

CodeDeploy環境を整える

CodeDeployエージェントのためのIAMロールを作成してEC2インスタンスに割り当てます。

$ cat codedeploy-ec2-trust.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ec2.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
$ aws iam create-role \
    --role-name test-laravel-ec2 \
    --assume-role-policy-document file://codedeploy-ec2-trust.json
$ cat codedeploy-ec2-permissions.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:Get*",
                "s3:List*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
$ policy_ec2=$( \
    aws iam create-policy \
        --policy-name test-laravel-ec2 \
        --policy-document file://codedeploy-ec2-permissions.json)
$ policy_ec2_arn=$(echo $policy_ec2 | jq -r ".Policy.Arn")
$ aws iam attach-role-policy \
    --policy-arn ${policy_ec2_arn} \
    --role-name test-laravel-ec2
$ aws iam create-instance-profile \
    --instance-profile-name test-laravel
$ aws iam add-role-to-instance-profile \
    --role-name test-laravel-ec2 \
    --instance-profile-name test-laravel
$ aws ec2 associate-iam-instance-profile \
    --instance-id ${ec2_instance_id} \
    --iam-instance-profile Name=test-laravel

EC2インスタンスにCodeDeployエージェントをインストールします。

$ ssh -i test-laravel.pem ec2-user@${ec2_ip}

$ sudo yum update
$ sudo yum install ruby -y 
$ sudo yum install wget -y
$ wget https://aws-codedeploy-ap-northeast-1.s3.amazonaws.com/latest/install
$ chmod +x ./install
$ sudo ./install auto

$ exit

CodeDeployのアプリケーションとデプロイグループを作成します。

$ cat deploy_trust_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "codedeploy.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
$ role_deploy=$(aws iam create-role --role-name test-laravel-deploy \
                      --assume-role-policy-document file://deploy_trust_policy.json)
$ role_deploy_arn=$(echo $role_deploy | jq -r ".Role.Arn")
$ aws iam attach-role-policy \
    --policy-arn arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole \
    --role-name test-laravel-deploy
$ aws deploy create-application --application-name test-laravel
$ aws deploy create-deployment-group \
    --application-name test-laravel \
    --deployment-group-name test-laravel \
    --deployment-config-name CodeDeployDefault.AllAtOnce \
    --ec2-tag-filters Key=Name,Value=test-laravel,Type=KEY_AND_VALUE \
    --service-role-arn ${role_deploy_arn}

デプロイスクリプトaws_deploy.shとCodeDeploy向けの構築定義ファイルappspec.ymlをLaravelプロジェクトに追加します。

$ cd test-laravel
$ mkdir bin
$ cat bin/aws_deploy.sh

aws_deploy.shの内容は下記です。

#!/bin/bash

deploy_path="/usr/share/nginx/html/test-laravel"

# デプロイ先
cd ${deploy_path} || exit 99

timestamp=$(date +%s)

# 必要なディレクトリを作成
mkdir -p ./shared
mkdir -p "./releases/${timestamp}"

# 作業ディレクトリにデプロイ
ln -nfs "${deploy_path}/releases/${timestamp}" ./release
cp -arf /tmp/test_laravel/* ./release/

# デプロイ対象外ファイルを用意
if [ ! -d ./shared/storage ]; then
  cp -arf ./release/storage ./shared/
  chmod 777 -R ./shared/storage
fi

chown -R nginx ./*

cd ./release || exit 99

# デプロイ対象外ファイルをシンボリックリンク化
rm -f .env
ln -nfs ${deploy_path}/.env ./.env
chown -h nginx ./.env
rm -rf ./storage
ln -nfs ${deploy_path}/shared/storage ./storage
chown -h nginx ./storage

# 各種コマンド実施
php artisan storage:link
php artisan view:clear
php artisan cache:clear
composer dump-autoload --optimize

cd ../ || exit 99

# デプロイ実施
ln -nfs "${deploy_path}/releases/${timestamp}" ./current
unlink ./release

# php-fpm/nginx再起動して適用
systemctl restart php-fpm.service
systemctl restart nginx.service

続きます。

$ chmod 755 bin/aws_deploy.sh
$ cat appspec.yml

appspec.ymlの内容は下記です。

version: 0.0
os: linux
files:
  - source: ./
    destination: /tmp/test_laravel
permissions:
  - object: /tmp/test_laravel
    pattern: "**"
    owner: nginx
hooks:
  AfterInstall:
    - location: bin/aws_deploy.sh
      timeout: 300

続きます。

$ git add -A
$ git commit -m "Add support for CodeDeploy"
$ git push origin master

$ cd ..

CodeBuild環境を整える

まずCodeBuildプロジェクトをEC2インスタンスと同じ環境にするためECRにAmazon Linux 2 AMI 2.0.20181114 x86_64 HVM gp2のdockerイメージを登録して使えるようにします。

$ ecr_login=$(aws ecr get-login --no-include-email)
$ echo $ecr_login > ecr_login.sh
$ chmod 755 ecr_login.sh
$ bash ./ecr_login.sh

$ ecr_repo=$(aws ecr create-repository --repository-name test-laravel)
$ ecr_repo_url=$(echo $ecr_repo | jq -r ".repository.repositoryUri")

$ docker pull amazonlinux:2.0.20181114
$ docker tag amazonlinux:2.0.20181114 ${ecr_repo_url}/test-laravel:latest
$ docker push ${ecr_repo_url}:latest

CodeBuildからアクセスできるようにECRを設定します。

$ cat ecr_policy.json
{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "CodeBuildAccess",
            "Effect": "Allow",
            "Principal": {
                "Service": "codebuild.amazonaws.com"
            },
            "Action": [
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "ecr:BatchCheckLayerAvailability"
            ]
        }
    ]
}
$ aws ecr set-repository-policy --repository-name test-laravel --policy-text file://ecr_policy.json

CodeBuildのIAMロールを作成します。

$ cat codebuild-trust-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "codebuild.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
$ role_build=$(aws iam create-role --role-name test-larabel-build \
                      --assume-role-policy-document file://codebuild-trust-policy.json)
$ role_build_arn=$(echo $role_build | jq -r ".Role.Arn")
$ aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
                             --role-name test-larabel-build

CodeBuildを作成します。

$ cat Source.json
{
  "type": "CODECOMMIT",
  "location": "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/test-laravel"
}
$ cat Artifacts.json
{
  "type": "NO_ARTIFACTS"
}
$ cat Environment_Template.json
{
  "type": "LINUX_CONTAINER",
  "image": ":ecr_repo_url::latest",
  "computeType": "BUILD_GENERAL1_SMALL"
}
$ cat Environment_Template.json | sed s/:ecr_repo_url:/$(echo ${ecr_repo_url} | sed "s/\//\\\\\//g")/ > Environment.json
$ aws codebuild create-project --name test-laravel \
                               --source file://Source.json \
                               --artifacts file://Artifacts.json \
                               --environment file://Environment.json \
                               --service-role ${role_build_arn}

CodeBuild向けの構築定義ファイルbuildspec.ymlをLaravelプロジェクトに追加します。

$ cd test-laravel
$ cat buildspec.yml

buildspec.ymlの内容は下記です。

version: 0.2

phases:
  install:
    commands:
      - |
          amazon-linux-extras install php7.2 -y
          yum install -y php-mbstring php-xml php-bcmath

          curl -sS https://getcomposer.org/installer | php
          cp composer.phar /usr/local/bin/composer
          ln -s /usr/local/bin/composer /usr/bin/composer

          yum install -y git
          yum install -y zip unzip
  build:
    commands:
      - rm -rf .git .gitignore README.md
      - composer install --verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader

artifacts:
  files:
    - '**/*'

続きます。

$ git add -A
$ git commit -m "Add support for CodeBuild"
$ git push origin master

$ cd ..

CodePipeline環境を整える

CodePipeline向けのIAMロールを作成します。

$ cat codepipeline-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "codepipeline.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
$ role_pipeline=$(aws iam create-role --role-name test-larabel-pipeline \
                      --assume-role-policy-document file://codepipeline-trust-policy.json)
$ role_pipeline_arn=$(echo $role_pipeline | jq -r ".Role.Arn")
$ aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
                             --role-name test-larabel-pipeline

アーティファクトに設定するS3バケットを作成します。*1

$ aws s3 mb s3://test-laravel-pipeline --region ap-northeast-1

CodePipelineを作成します。

$ cat pipeline_template.json
{
    "version": 1,
    "name": "test-laravel",
    "roleArn": ":role_pipeline_arn:",
    "artifactStore": {
        "type": "S3",
        "location": "test-laravel-pipeline"
    },
    "stages": [
        {
            "name": "Source",
            "actions": [
                {
                    "inputArtifacts": [],
                    "configuration": {
                        "PollForSourceChanges": "true",
                        "BranchName": "master",
                        "RepositoryName": "test-laravel"
                    },
                    "name": "Source",
                    "outputArtifacts": [
                        {
                            "name": "SourceArtifact"
                        }
                    ],
                    "actionTypeId": {
                        "version": "1",
                        "category": "Source",
                        "owner": "AWS",
                        "provider": "CodeCommit"
                    },
                    "runOrder": 1
                }
            ]
        },
        {
            "name": "Build",
            "actions": [
                {
                    "inputArtifacts": [
                        {
                            "name": "SourceArtifact"
                        }
                    ],
                    "configuration": {
                        "ProjectName": "test-laravel"
                    },
                    "name": "Build",
                    "outputArtifacts": [
                        {
                            "name": "BuildArtifact"
                        }
                    ],
                    "actionTypeId": {
                        "version": "1",
                        "category": "Build",
                        "owner": "AWS",
                        "provider": "CodeBuild"
                    },
                    "runOrder": 1
                }
            ]
        },
        {
            "name": "Deploy",
            "actions": [
                {
                    "inputArtifacts": [
                        {
                            "name": "BuildArtifact"
                        }
                    ],
                    "configuration": {
                        "DeploymentGroupName": "test-laravel",
                        "ApplicationName": "test-laravel"
                    },
                    "name": "Deploy",
                    "outputArtifacts": [],
                    "actionTypeId": {
                        "version": "1",
                        "category": "Deploy",
                        "owner": "AWS",
                        "provider": "CodeDeploy"
                    },
                    "runOrder": 1
                }
            ]
        }
    ]
}
$ cat pipeline_template.json | sed s/:role_pipeline_arn:/$(echo ${role_pipeline_arn} | sed "s/\//\\\\\//g")/ > pipeline.json
$ aws codepipeline create-pipeline --pipeline file://pipeline.json
$ pipeline=$(aws codepipeline get-pipeline --name test-laravel)
$ pipeline_arn=$(echo $pipeline | jq -r ".metadata.pipelineArn")

作成すると自動で実行が開始してmasterブランチがデプロイされます🎉 結果はコンソールで確認できます。

CodeBuildをキャッシュ対応する

このままでも動くのですが毎回composer installを実行するのは無駄です。composer.lockに変化がない場合は前回キャッシュしたvendorを使うようにします。

まずキャッシュを格納するs3バケットを作成します。*2

$ aws s3 mb s3://test-laravel-codebuild-cache --region ap-northeast-1

CodeBuildにキャッシュ設定を行います。

$ cat codebuild_cache.json
{
  "type": "S3",
  "location": "test-laravel-codebuild-cache"
}
$ aws codebuild  update-project --name test-laravel --cache file://codebuild_cache.json

Laravelプロジェクトのbuildspec.ymlを更新します。

$ cat buildspec.yml.patch

buildspec.yml.patchの内容は下記です。

--- buildspec.yml
+++ buildspec.yml
@@ -4,20 +4,36 @@
   install:
     commands:
       - |
-          amazon-linux-extras install php7.2 -y
-          yum install -y php-mbstring php-xml php-bcmath
+          yum install -y tar
+          if [ -e /tmp/composer.lock ] && [ -e /tmp/vendor.tar ]; then
+            diff /tmp/composer.lock ./composer.lock
+            if [ $? -eq 0 ]; then
+              tar xf /tmp/vendor.tar
+            fi
+          fi
+          if [ ! -e ./vendor ]; then
+            amazon-linux-extras install php7.2 -y
+            yum install -y php-mbstring php-xml php-bcmath

-          curl -sS https://getcomposer.org/installer | php
-          cp composer.phar /usr/local/bin/composer
-          ln -s /usr/local/bin/composer /usr/bin/composer
+            curl -sS https://getcomposer.org/installer | php
+            cp composer.phar /usr/local/bin/composer
+            ln -s /usr/local/bin/composer /usr/bin/composer

-          yum install -y git
-          yum install -y zip unzip
+            yum install -y git
+            yum install -y zip unzip
+          fi
   build:
     commands:
       - rm -rf .git .gitignore README.md
-      - composer install --verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader
-
+      - if [ ! -e ./vendor ]; then composer install --verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader; fi
+  post_build:
+    commands:
+      - cp composer.lock /tmp
+      - tar cf /tmp/vendor.tar vendor
 artifacts:
   files:
     - '**/*'
+cache:
+  paths:
+    - /tmp/composer.lock
+    - /tmp/vendor.tar

続きます。

$ patch -u ./test-laravel/buildspec.yml < buildspec.yml.patch

$ cd test-laravel
$ git add -A
$ git commit -m "Use CodeBuild cache"
$ git push origin master

$ cd ..

CodeBuildを実行するとS3バケットにvendorのキャッシュが作成されます。 結果はコンソールから確認できます。

$ aws codepipeline start-pipeline-execution --name test-laravel

再度CodeBuildを実行するとvendorのキャッシュが使われるので短い時間で完了します🎉 結果はコンソールから確認できます。

$ aws codepipeline start-pipeline-execution --name test-laravel

CloudWatch EventsでCodePipelineが自動実行されるよう設定する

CodeCommitを監視するCloudWatch Events向けのIAMロールを作成します。

$ cat cwe-trust-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Service": "events.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
      }
    ]
}
$ role_cwe=$(aws iam create-role --role-name test-laravel-cwe \
                      --assume-role-policy-document file://cwe-trust-policy.json)
$ role_cwe_arn=$(echo $role_cwe | jq -r ".Role.Arn")
$ aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
                             --role-name test-laravel-cwe

CloudWatch Eventsを作成します。

$ cat event_pattern_template.json
{
  "source": [
    "aws.codecommit"
  ],
  "detail-type": [
    "CodeCommit Repository State Change"
  ],
  "resources": [
    ":codecommit_arn:"
  ],
  "detail": {
    "event": [
      "referenceCreated",
      "referenceUpdated"
    ],
    "referenceType": [
      "branch"
    ],
    "referenceName": [
      "master"
    ]
  }
}
$ cat event_pattern_template.json | sed s/:codecommit_arn:/$(echo ${codecommit_arn} | sed "s/\//\\\\\//g")/ > event_pattern.json
$ aws events put-rule \
  --name "TestLaravel" \
  --event-pattern file://event_pattern.json \
  --role-arn ${role_cwe_arn}
$ aws events put-targets \
  --rule TestLaravel \
  --targets "Id"="1","Arn"="${pipeline_arn}","RoleArn"="${role_cwe_arn}"

これでmasterへのpushでCodePipelineが自動実行されるようになりました🎉🎉🎉 結果はコンソールから確認できます。

$ cat welcome.blade.php.patch
--- welcome.blade.php
+++ welcome.blade.php
@@ -81,7 +81,7 @@

             <div class="content">
                 <div class="title m-b-md">
-                    Laravel
+                    Hello World!
                 </div>

                 <div class="links">
$ patch -u ./test-laravel/resources/views/welcome.blade.php < welcome.blade.php.patch

$ cd test-laravel
$ git add -A
$ git commit -m "Test master push"
$ git push origin master

$ cd ..

しばらくしてブラウザ(http://${ec2_ip})からアクセスするとトップページの文字列がLaravelからHello World!に変わります。

まとめ

今回はプライベートな時間を利用してLaravelプロジェクトをCodeCommitのmasterブランチにpushしたらEC2に自動デプロイされる環境を構築する手順をまとめました。Code4兄弟でのデプロイ環境構築の経験はコンソールで少しありましたがCLIはほぼ無かったので勉強になりました。

構築した環境には改善した方が良い点もたくさんありますので今回の手順を土台にして色々と挑戦してみるのも良いかもしれません。

  • デプロイの前に自動テストが実行されるようにする
  • releasesの古いデプロイフォルダを自動的に削除する
  • IAMロールの設定を必要最小限にする
  • ロードバランサーを導入したりもうちょっと実用に耐えられるVPC構成にする
  • AWS CLIでもまだまだ保守大変そうなのでTerraformなどに置き換える
  • ECSやEKSなどもう少しモダンな構成にする
  • etc

*1:バケット名がすでに使われている場合は適当に変更します

*2:バケット名がすでに使われている場合は適当に変更します