技術的負債解消に向けたWordPressの一元管理とShifter移行で実現した運用最適化

はじめに

皆さんこんにちは。ファインディに転職してまもなく1年を迎える、CTO室/SREチームの安達(@adachin0817)です。今回は複数のWordPressサイトをShifterへ移行しましたので、その取り組みについてご紹介します。

Shifterとは

ja.getshifter.io

Shifterは、WordPressサイトを静的に変換・ホスティングできるマネージドサービスです。動的に動作するWordPressをShifterがビルドし、生成されたHTML/CSS/JavaScriptの静的ファイルをCDN経由で配信する構成となっています。主なメリットは次のとおりです。

  • セキュリティ強化:PHP実行環境を持たないため、WordPress本体やプラグインの脆弱性リスクが激減。
  • 運用負荷の軽減:インフラやWordPressのバージョンアップ管理が不要。
  • 表示速度の高速化:静的ファイルをCDN 経由で高速配信できるため、ユーザー体験の向上が期待。
  • 自動バックアップ・簡単ロールバック:管理画面から任意のタイミングで復元可能。

Shifterに移行する背景

  • 旧構成図

元々はAmazon Lightsail上の単一サーバーで複数のサイトを運用しており、画像ファイルやデータベースもすべて同一のインスタンス上で管理されていました。コーポレートサイト以外はPHPやプラグインのバージョン管理が行われておらず、明確な運用者もいなかったため、誰がメンテナンスしているのか分からない状況が続いていました。

また、コーポレートサイトに関しては情報システムチームが毎週のようにセキュリティアップデートを対応する必要があり、テーマのデプロイも自動化されておらず手動で行っていたため、運用負担が大きいという課題を抱えていました。

こうした背景に加え、会社全体でも定期的なバージョンアップ対応やセキュリティ強化に十分なリソースを確保することが難しく、改善が急務となっていました。そのような中、2024年に全社的な信頼性向上を目的として横断SREチームが発足しました。SREチームでは属人化していたWordPress環境を集約・統制することで、信頼性の向上を目指す取り組みが始まりました。

その解決策としてマネージドサービスへの移行を検討し、上記のメリットとコストが比較的安価のため、最終的にShifterを採用しました。次は、ローカル開発環境についてご紹介していきます。

開発環境の構築

ローカル開発環境では、コーポレートサイト以外の開発環境が整っていなかったため、Dockerを使って1から構築し直すことにしました。リポジトリ構成としては、WordPress本体をwordpress-coreリポジトリ(仮)で一元管理し、各サービスのテーマは個別のリポジトリで管理する方式を採用しました。これは、すべてを単一リポジトリに集約すると容量の肥大化や管理の複雑さを招くためです。

※以下仮としています

  • compose.yml
❯❯ cat docker-compose.yml
volumes:
  php-fpm-sock:

services:
  wp-nginx:
    container_name: wp-nginx
    build: docker/dev/nginx/
    image: wp-nginx
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./wordpress-core:/var/www/wp:delegated
      - ./site-corporate:/var/www/wp/corporate/wp-content/themes/site-corporate:delegated
      - ./site-enterprise01:/var/www/wp/enterprise01/wp-content/themes/site-enterprise01:delegated
      - ./site-enterprise02:/var/www/wp/enterprise02/enterprise/wp-content/themes/site-enterprise02:delegated
      - ./site-blog:/var/www/wp/blog/wp-content/themes/site-blog:delegated
      - php-fpm-sock:/var/run/php-fpm
    tty: true
    depends_on:
      - wp-app

  wp-app:
    container_name: wp-app
    build: docker/dev/app
    image: wp-app
    volumes:
      - ./wordpress-core:/var/www/wp:delegated
      - ./site-corporate:/var/www/wp/corporate/wp-content/themes/site-corporate:delegated
      - ./site-enterprise01:/var/www/wp/enterprise01/wp-content/themes/site-enterprise01:delegated
      - ./site-enterprise02:/var/www/wp/enterprise02/enterprise/wp-content/themes/site-enterprise02:delegated
      - ./site-blog:/var/www/wp/blog/wp-content/themes/site-blog:delegated
      - php-fpm-sock:/var/run/php-fpm
    tty: true
    depends_on:
      - wp-db

  wp-db:
    container_name: wp-db
    build: docker/dev/mysql
    image: wp-db
    command: --default-authentication-plugin=mysql_native_password
    ports:
      - '33066:3306'
    volumes:
      - ./mysql/db_data:/var/lib/mysql
      - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql

ShifterがPHP 8.1に対応していることから、Appコンテナ・Nginxコンテナ・MySQLコンテナも同じバージョンに揃えて構成しました。WordPressのバージョンや依存パッケージに差異があると、静的化後に想定外の動作になる恐れがあるためです。Shifterでは開発用のDocker環境も用意されていますが、できる限り本番と同等の構成になるよう意識して環境を整備しました。

さらに、Justfileを導入することで、hosts登録やmkcertによる自己署名SSL証明書発行、S3からのDBダンプ取得などのセットアップを自動化しました。複雑だった開発環境の構築手順が、今では just all コマンドを実行すれば素早く構築できるようになりました。ただ、運用していく中で、開発環境自体のバージョンアップもしなければなりませんが、今後GitHub Actionsで自動化していきたいと思っています。

  • Justfile
    • 開発用ドメイン設定
    • リポジトリのclone
    • mkcertの実行
    • wp-config.phpのコピー
    • Docker Composeの起動から停止
    • DBのリストア
setup-hosts:
    for hosts in "127.0.0.1 dev.site.co.jp" "127.0.0.1 dev.site.co.com" "127.0.0.1 dev-site2.com" "127.0.0.1 dev-site3.com"; do \
        if ! grep -q "$hosts" /etc/hosts; then \
            sudo bash -c "echo '$hosts' >> /etc/hosts"; \
        fi; \
    done

setup-mkcert:
    cd ~/www/wordpress-core && \
    brew install mkcert && \
    mkcert -install && \
    mkdir -p docker/dev/nginx/cert && \
    cd docker/dev/nginx/cert && \
    mkcert "*.hoge.co.jp"

clone-repos:
    @echo "Cloning repositories..."
    repos="site-corporate site-blog site-enterprise01 site-enterprise02"; \
    for repo in $repos; do \
        dir="$HOME/www/$repo"; \
        url="git@github.com:hoge/$repo.git"; \
        if [ ! -d "$dir" ]; then \
            git clone --depth 1 "$url" "$dir"; \
            echo "Cloned $repo repository."; \
        else \
            echo "$repo repository already exists. Pulling latest changes."; \
            cd "$dir" && git pull origin main; \
        fi; \
    done

copy-wp-config:
    for dir in "corporate" "site-blog/blog" "site-enterprise01/enterprise01" "site-enterprise02/enterprise02"; do \
        cd ~/www/wordpress-core/$dir && cp wp-config-dev.php wp-config.php; \
    done

start-docker:
    docker-compose up -d

stop-docker:
    docker-compose stop

down-docker:
    docker-compose down

restore-db profile:
    rm -rf ./db_dumps
    aws --profile={{profile}} s3 cp s3://hoge/dev/MySQL/ ./db_dumps --recursive --exclude "*" --include "*.sql"
    for file in ./db_dumps/*.sql; do \
        db_name=$(basename "$file" .sql); \
        echo "Copying $file to wp-db container"; \
        docker cp "$file" wp-db:/tmp/$(basename "$file"); \
        echo "Dropping and creating $db_name"; \
        docker exec wp-db mysql -u root -e "DROP DATABASE IF EXISTS \`$db_name\`; CREATE DATABASE \`$db_name\`;"; \
        echo "Restoring $db_name from /tmp/$(basename "$file")"; \
        docker exec wp-db sh -c "mysql -u root $db_name < /tmp/$(basename "$file")"; \
        echo "Cleaning up /tmp/$(basename "$file") from wp-db container"; \
        docker exec wp-db rm "/tmp/$(basename "$file")"; \
    done
    rm -rf ./db_dumps

all profile:
    just setup-hosts
    just clone-repos
    just copy-wp-config
    just start-docker
    just restore-db {{profile}}

Shifter移行とトラブルシューティング

Shifterへの移行にあたっては、公式ドキュメントにも記載されているとおり、All-in-One WP Migrationプラグインを使ったエクスポート・インポート方式が推奨されています。この方法を使えば、WordPressサイトを簡単に丸ごと移行できるのが大きなメリットです。実際に試してみたところ、基本的な移行作業はスムーズに完了しましたが、いくつか想定外の不具合も発生しました。以下にその内容を共有します。

独自テーマのページネーションが正しく動作しない

コーポレートサイトのページネーションが正しく機能しないという事象が発生しました。Shifterでは、静的サイトを生成する際にWordPress REST API を通じて対象のURL一覧(JSON形式)を取得し、それをもとに静的化を行います。しかしながら、WordPressのアーカイブページのページネーションはデフォルト設定のままだと検出されず、静的化の対象から漏れてしまうことがあります。この問題に対しては、Shifterが提供する ShifterURLS::AppendURLtoAll を使うことで、手動で静的化対象のURLを追加が可能です。次のようにfunctions.phpに追加し、ページネーションURLを明示的に含めることで対応しました。

  • function.php
function my_append_urls( $urls ) {
    $limit = get_option('posts_per_page');
    $args = array(
      'post_type' => 'post',
      'post_status' => 'publish',
      'posts_per_page' => -1,
      'fields' => 'ids',
      'no_found_rows' => true,
    );
    $the_query = new WP_Query($args); 
    if ($the_query->have_posts()) {
      $last_page = ceil(count($the_query->posts) / $limit);
      for ($i = 2; $i <= $last_page; $i++) {
        $urls[] = home_url('/news/page/' . $i . '/');
      }
    }
    return $urls;
  }
  add_action( 'init', function(){
    add_filter( 'ShifterURLS::AppendURLtoAll', 'my_append_urls' );
  } );

一部画像が表示されない

All-in-One WP Migrationを使った移行ではスムーズに進みましたが、日本語ファイル名の画像が表示されないという問題が発生しました。Shifterへの移行後、原因を調査したところ、画像ファイル名が日本語の場合に限って読み込めないというバグを確認できました。数枚ほどだったので、対象のファイルを手動でリネームし、再アップロードをする必要がありました。

サブディレクトリでシェアボタンがShifterの仮ドメインになってしまう

既にCloudFrontにカスタムドメインが設定されている場合、Shifter 側のCDNに同じカスタムドメインを適用できません。このようなケースでは、Shifterで生成された静的コンテンツのみを対象に、CloudFront経由でカスタムドメイン配下のサブディレクトリに公開する必要があります。 /blog のようにサブディレクトリでの公開が必要な場合、 shifter-cli を使って --no-shifter-cdn オプションを指定することで、ShifterのCDN配信を無効化し、自前のCloudFront経由でのホスティングが可能になります。

CloudFrontのオリジンドメインにShifterが提供するCloudFront URLを指定し、キャッシュポリシーを無効化に設定することで、Shifterで生成された静的サイトをそのまま公開できます。最後にShifterでデプロイを実行することで、シェアボタンやOGPのリンク先にShifterの仮ドメインが含まれることはなくなりました。

  • ドメイン紐づけ
$ npm install -g @shifter/cli
$ shifter domain:attach --username hoge --password hoge --site-id hoge --domain hoge.com --no-shifter-cdn
  • terraform/cloudfront.tf
data "aws_cloudfront_cache_policy" "caching_disabled" {
  name = "Managed-CachingDisabled"
}
 
resource "aws_cloudfront_distribution" "hoge" {
  provider = aws.us-east-1
 
  origin {
    domain_name = "hoge.cloudfront.net"
    origin_id   = "blog"
    origin_path = ""
 
    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
 
  ordered_cache_behavior {
    allowed_methods        = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cache_policy_id        = data.aws_cloudfront_cache_policy.caching_disabled.id
    cached_methods         = ["GET", "HEAD"]
    compress               = true
    path_pattern           = "blog/*"
    target_origin_id       = "blog"
    viewer_protocol_policy = "https-only"
  }
 
  ordered_cache_behavior {
    allowed_methods        = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cache_policy_id        = data.aws_cloudfront_cache_policy.caching_disabled.id
    cached_methods         = ["GET", "HEAD"]
    compress               = true
    path_pattern           = "blog/"
    target_origin_id       = "blog"
    viewer_protocol_policy = "https-only"
  }
}

これらトラブルシューティングを解決したことにより、次はテーマのデプロイ方法について説明したいと思います。

Shifter Github Plugin/Theme Installedでのテーマデプロイ

ShifterではGitHub経由でテーマをデプロイするための公式プラグイン「Shifter GitHub Plugin/Theme Installed」が提供されています。このプラグインを利用することで、次の手順でテーマの更新が可能になります。

  1. テーマをgit tagでバージョン付け
  2. git pushでタグを反映
  3. テーマ一式を .zip に圧縮し、GitHubの Releases にアップロード
  4. Shifter側で該当リリースの.zip を取得し、テーマを最新化

もともとは複数のテーマをモノレポで管理しようとしていましたが、Shifterの仕様上、1テーマ、1プロジェクト(単一リポジトリ)が前提となります。さらに、GitHub Actionsを活用してリリースの自動化も実装しており、手動作業を減らして運用効率を向上させました。

  • github/workflows/release.yaml
name: Tag and Release
 
on:
  push:
    branches: [main]
 
jobs:
  tag-and-release:
    name: Tag and Release
 
    runs-on: ubuntu-latest
 
    permissions:
      contents: write
 
    defaults:
      run:
        working-directory: "./"
 
    steps:
    - uses: actions/checkout@v4
 
    - name: Fetch tags
      run: git fetch --tags
 
    - name: Get latest tag
      id: get_latest_tag
      run: |
        TAG=$(git tag --sort=-v:refname | head -n 1 || echo "0.0.0")
        echo "Latest tag: $TAG"
        echo "latest_tag=$TAG" >> $GITHUB_ENV
 
    - name: Calculate next tag
      id: calculate_next_tag
      run: |
        IFS='.' read -r MAJOR MINOR PATCH <<< "${{ env.latest_tag }}"
        NEXT_TAG="$MAJOR.$MINOR.$((PATCH+1))"
        echo "Next tag: $NEXT_TAG"
        echo "next_tag=$NEXT_TAG" >> $GITHUB_ENV
 
    - name: Create new tag
      run: |
        git tag ${{ env.next_tag }}
        git push origin ${{ env.next_tag }}
 
    - name: create archive
      env:
        PACKAGE_NAME: "hoge-theme"
        FILES_TO_ARCHIVE: "*"
      run: |
        sed -i -e "s/{release version}/${{ env.next_tag }}/g" ./style.css
        zip -r ${PACKAGE_NAME}.zip ${FILES_TO_ARCHIVE}
 
    - name: Upload to GitHub Release
      env:
        PACKAGE_NAME: "hoge-theme"
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        GOPATH: /home/runner/go
      run: |
        go install github.com/tcnksm/ghr@latest
        ${GOPATH}/bin/ghr \
          -b "Release ${{ env.next_tag }}" \
          -replace \
          "${{ env.next_tag }}" \
          "${PACKAGE_NAME}.zip"
  • style.css
Version: {release version}

現在の運用体制について

Shifterのアカウント管理は情報システムチームが担当し、テーマの修正はエンジニアやSREチームが対応しています。また、記事の公開やコンテンツの更新は広報チームやマーケティングチームが行っており、各チームの役割が明確に分担されるようになりました。

移行してみて

Shifter導入によって、SREチームだけでなく非エンジニアのメンバーでも運用できる環境が整いました。Shifterのダッシュボードはシンプルで直感的に操作でき、バックアップの復元も簡単です。また、静的ファイル化によりPHPの脆弱性リスクが排除され、HTMLとCSSのみで構成されることでセキュリティ面でも安心できるようになりました。さらに、CDNが標準で組み込まれているため、パフォーマンスの向上も実感しています。一方で気になったところは記事をリリースするまで、毎回Shifter側で静的化デプロイをしなければならないので、反映までに時間がかかるといったところです。

さらに、1つのShifterアカウントで複数のサイトを一元管理できる点も大きなメリットです。以前はサイトごとにユーザーアカウントを発行していたため管理が煩雑でしたが、現在は運用効率が大幅に向上し、ユーザー棚卸しも簡単になりました。また、デジタルキューブ様の導入事例でも紹介していただきましたので、ぜひご覧ください。

ja.getshifter.io

まとめ

長らく技術的負債となっていたWordPress環境でしたが、SREチームを中心にShifterへの移行を進めたことで、信頼性の向上と一元管理を実現できました。自分自身、初めてShifterを導入するにあたり、静的化に伴う挙動の違いなど、試行錯誤する場面も多く苦労しました。しかし、今では導入して本当に良かったと感じています。今後は新たなメディアを立ち上げる際もShifterを利用し、SREチームを通じた管理フローを社内で徹底していきたいと思っています。

最後まで読んでいただきありがとうございました。SREチームは絶賛募集しているので、カジュアル面談ぜひお待ちしております!🙏

herp.careers

最後に

aws.amazon.com

AWS Summit Japan 2025で登壇することになりました!Security LakeとAIの活用についてお話します!6/26(木) 12:40~行いますので、ご都合合う方はぜひご参加をお待ちしております!