CUEBiC TEC BLOG

キュービックTECチームの技術ネタを投稿しております。

Wordpress自動同期ツール作ってみたズラ

SREのkmsn17です。前回は[ラズパイ](https://cuebic.hatenablog.com/entry/raspberrypi) について書きました。今回ですが、WordPress自動同期ツールについて記載したいと思います。

WordPressとは!?

ブログ、ホームページなどのサイトの作成のために使うContents Management Systemです。CMSと言われるものです。

なぜ、作ったの?

キュービックでは、100メディア弱ありますがそれらは全てWordPressでできています。検証も合わせると100台を超えると思います。ビジネスサイド側が、本番で書いた記事を検証にも書き起こす作業をしており、そういった作業コストを削減するために作りました。また、削減した時間を別の作業や思考作業にも使ってもらいたい思いもあります。Web上で、ビジネスサイド側が好きなときに操作していつでも同期ができるものです。

システムについて

PHPで書いてます。Web上で表示するPHPファイル、バックグラウンド側で動くPHPファイルに分けてます。こちらですが、 実際に同期を実施するプログラムはbashで行っています。セキュリティ上、web画面からbashが動けるようにしてはいけないので分けてます。こちらのプログラムはWeb上に表示して、同期するメディアを選択する内容になっています。

フロント側で動くプログラム

 1 <!DOCTYPE html>
  2 <html lang="ja">
  3 <head>
  4 <meta charset="utf-8">
  5 <title>同期画面</title>
  6 <style>
  7 .message {
  8 background-color: #f9e8d0;
  9 padding: 10px;
 10 border-radius: 5px;
 11 text-align: center;
 12 font-size: 18px;
 13 font-weight: bold;
 14 color: blue;
 15 }
 16 .button:hover {
 17 background-color: #ffd700;
 18 color: white;
 19 }
 20 </style>
 21 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet"     integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anony    mous">
 22 </head>
 23 <body>
 24 <?php
 25 $action = isset($_GET['action']) ? $_GET['action'] : '';
 26 ?>
 27 <div class="message">同期するメディアのジャンルを選んでください</div>
 28
 29 <?php if ($action === 'hoge-test-dev' || $action === 'dev-test-dev'): ?>
 30 <?php if ($action === 'hoge-test-dev'): ?>
31 <div>【本番】<a href="https://hoge-test.dev.kmsn.work/">https://hoge-test.dev.kmsn.work/</a> →【検証】<a href="https://kmsn-dev.dev.kmsn.work">https://kmsn-dev.dev.kmsn.work</a> に同期しますか?</div>
 32 <?php elseif ($action === 'dev-test-dev'): ?>
 33 <div>【本番】<a href="https://dev-test.dev.kmsn.work/">https://dev-test.dev.kmsn.work/</a>     →【検証】<a href="https://dev-kmsn.dev.kmsn.work">https://dev-kmsn.dev.kmsn.work</a> に同
    期しますか?</div>
 34 <?php endif; ?>
 35
 36
 37 <form method="post" action="SyncTool.php">
 38 <input type="hidden" name="action" value="<?php echo $action; ?>">
 39 <button class="btn btn-primary" type="submit" name="syncButton">同期する</button>
 40 </form>
 41
 42
 43 <button onclick="history.back()" class="btn btn-primary">戻る</button>
 44
 45
 46
 47
 48 <?php elseif ($action === 'hoge-dev'): ?>
 49 <a href="?action=hoge-test-dev" class="btn btn-primary">hoge-test-dev</a>
 50 <a href="?action=dev-test-dev" class="btn btn-primary">dev-test-dev</a>
 51 <button onclick="history.back()" class="btn btn-primary">戻る</button>
 52
 53 <?php elseif ($action === 'cuebic-dev'): ?>
 54 <a href="?action=hskn" class="btn btn-primary">hskn</a>
 55 <a href="?action=mrrn" class="btn btn-primary">mrrn</a>
 56 <button onclick="history.back()" class="btn btn-primary">戻る</button>
 57
 58
 59 <?php elseif ($action === 'hskn'): ?>
60 <div>【本番】<a href="https://hskn-media.stg.kmsn.work/">https://hskn-media.stg.kmsn.work/    </a> →【検証】<a href="https://kmhoge-media.stg.kmsn.work/">https://kmhoge-media.stg.kmsn.work</a> に同期する?</div>
 61   <form method="post" action="SyncTool.php">
 62     <input type="hidden" name="action" value="hskn">
 63     <button class="btn btn-primary" type="submit" name="syncButton">同期する</button>
 64   </form>
 65
 66   <button onclick="history.back()" class="btn btn-primary">戻る</button>
 67
 68
 69 <?php elseif ($action === 'mrrn'): ?>
 70 <div>【本番】<a href="https://mrrn-media.stg.kmsn.work/">https://mrrn-media.stg.kmsn.work/    </a> →【検証】<a href="https://kmhoge-media.stg.kmsn.work/">https://kmhoge-media.stg.kmsn.work</a> に同期する?</div>
 71   <form method="post" action="SyncTool.php">
 72     <input type="hidden" name="action" value="mrrn">
 73     <button class="btn btn-primary" type="submit" name="syncButton">同期する</button>
 74   </form>
 75
 76   <button onclick="history.back()" class="btn btn-primary">戻る</button>
 77
 78
 79 <?php else: ?>
 80 <a href="?action=hoge-dev" class="btn btn-primary">hoge-devメディア</a>
 81 <a href="?action=cuebic-dev" class="btn btn-primary">cuebic-devメディア</a>
 82
 83 <?php endif; ?>
 84
 85
 86 </body>
 87 </html>

バックグラウンド側で動くプログラムについて

SSMを使ってコマンドを動かすようにしてます。多重同期防止のためDynamoDBを使っています。同期するメディアを選択して、同期ボタンを押したあとDynamoDB上に選択したメディアが値として設定されて、その間多重同期の防止が動きます。メディア毎に多重同期の防止が動けるようにしたので、別のメディアは動きます。

1 <!DOCTYPE html>
  2 <html lang="ja">
  3 <head>
  4 <meta charset="utf-8">
  5 <title>CB WordPress Sync</title>
  6 <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet"     integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anony    mous">
  7 </head>
  8 <body>
  9 <?php
 10   // SlackのAPIを使用して通知を送信する関数
 11   function sendSlackNotification($accessToken, $channel, $message) {
 12       $url = 'https://slack.com/api/chat.postMessage';
 13
 14       $headers = [
 15           'Authorization: Bearer ' . $accessToken,
 16           'Content-Type: application/json',
 17       ];
 18
 19       $payload = json_encode([
 20           'channel' => $channel,
 21           'text' => $message,
 22       ]);
 23
 24       $ch = curl_init();
 25       curl_setopt($ch, CURLOPT_URL, $url);
 26       curl_setopt($ch, CURLOPT_POST, true);
 27       curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 28       curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
 29       curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
 30
 31       $result = curl_exec($ch);
 32       curl_close($ch);
33
 34       return $result;
 35   }
 36
 37   require 'vendor/autoload.php'; // AWS SDK for PHPのautoloadファイルを読み込む
 38
 39   use Aws\Ssm\SsmClient; // SSM(Systems Manager)クライアントを使用する
 40   use Aws\DynamoDb\DynamoDbClient; // DynamoDBクライアントを使用する
 41
 42   // AWS SSMクライアントを初期化
 43   $client = new SsmClient([
 44       'version' => 'latest',
 45       'region' => '', // AWSリージョンを指定
 46   ]);
 47
 48   // AWS DynamoDBクライアントを初期化
 49   $dynamoDbClient = new DynamoDbClient([
 50       'version' => 'latest',
 51       'region' => '', // AWSリージョンを指定
 52   ]);
 53
 54   // POSTリクエストで送信された'action'の値を受け取る
 55   $action = isset($_POST['action']) ? $_POST['action'] : '';
 56
 57   // DynamoDBに同期中フラグをセット
 58   $tableName = 'hoge-dev'; // DynamoDBのテーブル名を設定
 59   $lockKey = $action; // ロックのキーを設定
 60
 61   try{
 62
 63     $item= $dynamoDbClient->getItem([
 64       'TableName' => $tableName,
 65       'Key' => [
 66           'LockTable' => ['S' => $lockKey]
67       ]
 68     ]);
 69
 70     if (isset($item['Item']) ) { // Itemが存在する場合
 71       echo '<div class="message">前の人が同期を実施中です。完了するまでslackの通知を見てお待ちください
    。トップ画面に戻るには、<a href="https://hoge-test.dev.kmsn.work/hogehoge/HogeMedia.php"> https://hoge-test.dev.kmsn.work/hogehoge/HogeMedia.php</a>にアクセスしてください</div>';
 72     }else{ // Itemが存在しない場合
 73       // DynamoDBにItemを追加して同期中であることを示す。
 74       $dynamoDbClient->putItem([
 75               'TableName' => $tableName,
 76               'Item' => [
 77                   'LockTable' => ['S' => $lockKey]
 78               ],
 79       ]);
 80
 81       // ディレクトリの中のファイルをスキャン
 82       $directoryPath = "/XXXXX/XXXXX/HogeMedia/profiles/"    ;
 83       $files = array_diff(scandir($directoryPath), array('..', '.')); // 現在と親のディレクトリを除外
 84
 85       // $mediaMap の連想配列を動的に作成
 86       $mediaMap = [];
 87       foreach ($files as $file) {
 88           if (pathinfo($file, PATHINFO_EXTENSION) == 'yml') {
 89               $filenameWithoutExt = pathinfo($file, PATHINFO_FILENAME);
 90               $mediaMap[$filenameWithoutExt] = $filenameWithoutExt;
 91           }
 92       }
 93
 94
 95       // 連想配列から該当のアクションのymlファイル名を取得
 96       if (array_key_exists($action, $mediaMap)) {
          $media = $mediaMap[$action];
 98           $command = "bash /xxxxx/HogeMedia/sync/hoge.sh /xxxxx/HogeMedia/profiles/${hoge}.yml";
 99           $commands = [$command];
100       } else {
101           echo "不正なアクションが指定されました。";
102           exit;
103       }
104
105
106       // SSMコマンドを実行
107       $result = $client->sendCommand([
108           'DocumentName' => 'AWS-RunShellScript', // 実行するドキュメントの名前
109           'Parameters' => [
110               'commands' => $commands
111           ],
112           'Targets' => [
113               [
114                   'Key' => 'InstanceIds',
115                   'Values' => ['XXXXXXXXXXXXXXXX'] // コマンドを実行するインスタンスのIDを指定
116               ]
117           ],
118           'MaxConcurrency' => '2', // 1度に同時に実行するインスタンス数の上限を設定
119           'MaxErrors' => '0', // 同時実行中に許容されるエラー数の上限を設定
120       ]);
121
122       // 同期中メッセージを表示
123       echo '<div class="message" style="background-color: Bisque; color: black; font-size: 50px;">同期
    中です。トップ画面に戻るには、<a href="https://hoge-test.dev.kmsn.work/hogehoge/HogeMedia.php">https://hoge-test.dev.kmsn.work/hogehoge/HogeMedia.php</a>にアクセスしてください</div>';
124
125       // コマンドの実行結果を取得
126       $commandId = $result['Command']['CommandId'];
127
128       // Slackに通知を送信
129       $slackAccessToken = ''; // SlackのAPIトークンを設定
130       $slackChannel = ''; // 送信先のSlackチャンネルを設定(# から始まる)
131
132       $slackMessage = "'$media'から同期中です。今しばらくお待ちください"; // 通知するメッセージを設定
133
134       // Slack通知を送信
135       $result = sendSlackNotification($slackAccessToken, $slackChannel, $slackMessage);
136     }
137
138   }catch(Exception $e){
139     echo $e;
140   }
141 ?>
142 </body>
143 </html>

DynamoDBの画面です。選択されたメディア名が、LockTable上に値として設定される箇所です。

bashの中身

WordPressの差分同期のコマンドをbashで表示してます。ymlファイルを読み込んで、引数を元に各メディアの同期を実施しています。同期完了後、slackで通知して、最後にDynamoDBのItem値を削除しています。

 1 #!/bin/bash
  2
  3 set -e -o pipefail
  4
  5 echo -e "\e[1;36;40m設定確認##########################################\e    [m"
  6 echo -e "\e[1;36;40m【src_media】$(yq -r .src_media $1)\e[m"
  7 echo -e "\e[1;36;40m【dest_media】$(yq -r .dest_media $1)\e[m"
  8 echo -e "\e[1;36;40m【src_media_url】$(yq -r .src_media_url $1)\e[m"
  9 echo -e "\e[1;36;40m【dest_media_url】$(yq -r .dest_media_url $1)\e[m"
 10 echo -e "\e[1;36;40m設定確認##########################################\e    [m"
 11
 12 source /xxxx/xxxx/.bash_profile
 13 BACKUP_MD_NAME=$(yq -r .src_media $1)
 14 DEST_MD_NAME=$(yq -r .dest_media $1)
 15 BACKUP_MD_NAME_URL=$(yq -r .src_media_url $1)
 16 DEST_MD_NAME_URL=$(yq -r .dest_media_url $1)
 17 WORK_DIR=/xxxxx/xxxxx/xxxxx/xxxxx/xxxxx/xxxxx/xxxxx/xxxxx
 18 WORDPRESS_DIR=/xxxxx/xxxxx/xxxxx/xxxxx
 19
 20 echo \"${WORK_DIR}/${BACKUP_MD_NAME}\" | xargs -I {{}} sh -c 'sudo mkdir {{}} && sudo chown www-data:www-data {{}}'
 21 sudo -u www-data wp db export ${WORK_DIR}/${BACKUP_MD_NAME}/dump.sql --path=${WORDPRESS_DIR}/${BACKUP_MD_NAME}/DocumentRoot --set-gtid-purged=OFF
 22 sudo -u www-data rsync -av --delete --exclude=/.htaccess  ${WORDPRESS_DIR}/${BACKUP_MD_NAME}/DocumentRoot/ user名@移行先のサーバのIPアドレス:${WORDPRESS_DIR}/${DEST_MD_NAME}/DocumentRoot/
 23 sudo -u www-data wp db import ${WORK_DIR}/${BACKUP_MD_NAME}/dump.sql user名@移行先サーバのIPアドレス:${WORDPRESS_DIR}/${DEST_MD_NAME}/DocumentRoot/
 24 sudo rm -rf ${WORK_DIR}/${BACKUP_MD_NAME}
 25
 26
 27 # Slackに通知を送信する関数
 28 function send_slack_notification {
 29     local slack_token=""
 30     local channel="#"
 31     local message="$1"
 32
 33     curl -X POST -H "Authorization: Bearer $slack_token" -H 'Content-type: application/json' --data "{
 34         \"channel\": \"$channel\",
 35         \"text\": \"$message\"
 36     }" "https://slack.com/api/chat.postMessage"
 37 }
 38
 39 # 通知メッセージを作成
 40 notification_message="$BACKUP_MD_NAME_URL から $DEST_MD_NAME_URL への同期が完了しました。反映するまで10分程お待ちください"
 41
 42 # Slackに通知を送信
 43 send_slack_notification "$notification_message"
 44
 45 # dynamoDBのItemを削除
 46 aws dynamodb delete-item --table-name hoge-dev --key '{"LockTable": {"    S": "'$BACKUP_MD_NAME'"}}' --region awsリージョン名
 47

ymlファイルの中身

ymlファイルの中身です

src_media: hoge-test-dev
dest_media: kmsn-dev-dev
src_media_url: https://hoge-test.dev.kmsn.work/
dest_media_url: https://kmsn-dev.dev.kmsn.work/

総括

現在ですが、システムテストをしていまして、まだ社内では公開してないです。今回初めて、セキュリティやオペレーションミスを防ぐことを考えてプログラムを書きまして色々と勉強になりました。当初はif文で〜の場合の条件式を書きまくってハードコードになりました。次は、0からWebアプリケーションを作れるようにしていきたいです。

お知らせ!!

キュービックでは、一緒に働いてくれる仲間を募集しています。興味がある方はカジュアル面談Webエンジニアから応募をお願いします。ご応募お待ちしております。 herp.careers