Xサーバーで利用していたWordPressをCloud Runに移行してみた

## 移行しようと思ったきっかけと背景

個人的な話になりますが、2018年頃からXサーバーにて使い始めたWordPressの個人ブログをCloud Runに移行してみました(閉鎖するので移行してみたものの本運用はしません)

注：本題とは関係ない自分語りが冒頭にありますので、ご興味ない方は飛ばしてください

閉鎖する個人ブログですが、色々研究と工夫をして大体250記事近く書いてみた結果、特定の記事で検索結果の上位をとれたり、月間PVが最大で1万5千~2万ほどあったり、GoogleAdsenseの広告収入等も少々入っていたり。ほぼ素人だった者が一生懸命頑張ったなりにそこそこ成果は出ていたのですが。

2020年頃からエンジニア転職して、業務で扱う技術のキャッチアップに追われて全く更新できなくなりました。今後も更新するつもりはないので閉鎖しようと決意。ただせっかくなので、閉鎖する前に前職でよく使っていたCloudRunでWordPress運用できそうだなと考え、個人開発的なノリで実験的に移行してみようと思ったのが今回移行した背景です。

ちなみに放置している今でも僅かに広告収入自体は発生しているようですが、固定費のレンタルサーバー代 - 広告収入 = 良い方向に見積もっても月々マイナス500円くらいです。もしうまくコストを抑えたら、放置していても問題ないかもという願望もありましたが、調査の結果トントンにするのは難しく断念することに..

## 今回の記事で書くこと・書かないこと

<書くこと>

* XサーバーからCloud Runへの移行の流れ
* Google Cloud構築用のterraformコード
* 簡単な構成、概要説明など

<書かないこと>

* Cloud RunなどのGoogle Cloudリソースの基本的な使い方等
* 細かい移行手順

## 大まかな流れ

* Xサーバーから保存しているWordPressのデータをエクスポートする
* Google Cloudで使うインフラリソースを構築する
* Cloud SQLにDBをインポートする
* Cloud Storageにデータをインポートする

以下の記事等を参考に進めました
https://zenn.dev/google_cloud_jp/articles/cloudrun-wordpress
https://cloud.google.com/blog/ja/products/serverless/introducing-cloud-run-volume-mounts
https://cloud-ace.jp/column/detail356/


## 基本構成

以下のGoogle Cloudリソースを構築します

* Cloud Run
* Cloud SQL
* Cloud Storage
* Artifact Registry
* Secret Manager
* IAM

イメージ図
![スクリーンショット 2024-09-29 18.01.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985033/572e96fd-5eac-be74-d91f-4e1bd7ab6762.png)


### Xサーバーから保存しているWordPressのデータをエクスポートする

以下のファイルをバックアップしてローカル環境に保存します

* WordPressサイトファイル
* データベース (phpMyAdmin)

https://www.xserver.ne.jp/manual/man_install_transfer_wordpress.php

### Google Cloudで使うインフラリソースを構築する

ここからは実際にterraformを使ってインフラリソースを作成します

#### terraformの構成・コード

以下のように記述しました

ディレクトリ構成

```
└── wordpress
    ├── tf
    │   ├── db.tf
    │   ├── gcp-credential.json
    │   ├── provider.tf
    │   ├── run.tf
    │   ├── terraform.tfstate
    │   ├── terraform.tfstate.backup
    │   ├── variables.tf
    │   └── wp.tfvars
    └── wp
        ├── 000-default.conf
        ├── Dockerfile
        ├── apache2.conf
        └── public_html
```

Google Cloudリソース側のterraformコード

Cloud SQL, Cloud Strage
```
resource "google_sql_database_instance" "wp_db" {
  name             = "wp-db"
  database_version = "MYSQL_8_0"
  region           = var.region

  settings {
    tier = "db-f1-micro"
  }

  lifecycle {
    ignore_changes = [
      settings[0].version
    ]
  }

  deletion_protection = false
}

resource "google_sql_database" "default" {
  name     = "***com_wp1"
  instance = google_sql_database_instance.wp_db.name
}

resource "google_sql_user" "root" {
  name     = "root"
  instance = google_sql_database_instance.wp_db.name
  password = var.db_password
}

resource "google_sql_user" "wp_user" {
  name     = var.db_user
  instance = google_sql_database_instance.wp_db.name
  password = var.db_password
}

resource "google_storage_bucket" "wp_media" {
  name          = "${var.project_id}-wp-media"
  location      = var.region
  force_destroy = true
}

```

Cloud Run, Artifact Registry, Secret Manger, IAM

```
resource "google_artifact_registry_repository" "repo" {
  location      = var.region
  repository_id = "***-repo"
  format        = "DOCKER"
  description   = "Docker repository"
}

resource "google_project_service" "artifact_registry" {
  project = var.project_id
  service = "artifactregistry.googleapis.com"
}

data "google_project" "project" {
  project_id = var.project_id
}

resource "google_secret_manager_secret" "wp_password" {
  secret_id = "wp-db-pass"

  replication {
    auto {}
  }
}

resource "google_secret_manager_secret_version" "wp_password" {
  secret      = google_secret_manager_secret.wp_password.name
  secret_data = var.db_password
}

resource "google_project_service" "secretmanager" {
  project = var.project_id
  service = "secretmanager.googleapis.com"
}

resource "google_service_account" "run_service_account" {
  account_id   = "run-sa"
  display_name = "Cloud Run Service Account"
}

resource "google_storage_bucket_iam_member" "bucket_viewer" {
  bucket = "***-wordpress-media"
  role   = "roles/storage.objectAdmin"
  member = "serviceAccount:${google_service_account.run_service_account.email}"
}

resource "google_project_iam_member" "secret_accessor" {
  project = var.project_id
  role    = "roles/secretmanager.secretAccessor"
  member  = "serviceAccount:${google_service_account.run_service_account.email}"
}

resource "google_secret_manager_secret_iam_member" "secret-access" {
  secret_id = google_secret_manager_secret.wp_password.id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.run_service_account.email}"
  depends_on = [google_secret_manager_secret.wp_password]
}

resource "google_cloud_run_v2_service" "wp_***" {
  name     = "wp-***"
  location = var.region
  ingress  = "INGRESS_TRAFFIC_ALL"

  template {
    service_account = google_service_account.run_service_account.email

    vpc_access {
      egress = "PRIVATE_RANGES_ONLY"
      network_interfaces {
        network    = "default"
        subnetwork = "default"
      }
    }

    scaling {
      max_instance_count = 1
      min_instance_count = 0
    }

    containers {
      image = "${var.region}-docker.pkg.dev/${var.project_id}/***-repo/***-com"
      name  = "***-com-1"

      ports {
        container_port = 80
      }

      env {
        name  = "DB_HOST"
        value = "**.**.**.**"
      }

      env {
        name  = "DB_USER"
        value = var.db_user
      }

      env {
        name = "DB_PASSWORD"
        value_source {
          secret_key_ref {
            secret  = google_secret_manager_secret.wp_password.secret_id
            version = "latest"
          }
        }
      }

      env {
        name  = "DB_NAME"
        value = var.db_name
      }

      resources {
        cpu_idle = true
        limits = {
          cpu    = "1000m"
          memory = "512Mi"
        }
      }

      volume_mounts {
        name       = "wp-media"
        mount_path = "/var/www/html"
      }

      volume_mounts {
        name       = "cloudsql"
        mount_path = "/cloudsql"
      }
    }

    volumes {
      name = "wp-media"
      gcs {
        bucket = "***-wordpress-media"
        read_only = false
      }
    }

    volumes {
      name = "cloudsql"
      cloud_sql_instance {
        instances = [google_sql_database_instance.wp_db.connection_name]
      }
    }
  }

  lifecycle {
    ignore_changes = [
      annotations,
      labels,
      client,
      etag,
      generation,
      last_modifier,
      latest_created_revision,
      latest_ready_revision,
      observed_generation,
      terminal_condition,
      update_time,
      template[0].containers
    ]
  }

  depends_on = [
    google_sql_database_instance.wp_db,
    google_artifact_registry_repository.***_repo,
    google_project_service.secretmanager
  ]
}

data "google_iam_policy" "noauth" {
  binding {
    role    = "roles/run.invoker"
    members = ["allUsers"]
  }
}

resource "google_cloud_run_service_iam_policy" "noauth" {
  location = google_cloud_run_v2_service.wp_***.location
  project  = google_cloud_run_v2_service.wp_***.project
  service  = google_cloud_run_v2_service.wp_***.name

  policy_data = data.google_iam_policy.noauth.policy_data
}

resource "google_project_service" "run" {
  project = var.project_id
  service = "run.googleapis.com"
}
```

環境変数
```
variable "project_id" {
  type = string
}

variable "region" {
  type    = string
  default = "asia-east1"
}

variable "db_password" {
  type = string
}

variable "db_name" {
  type = string
}

variable "db_user" {
  type = string
}
```

WP側のコード

Dockerfile
```
FROM wordpress:latest

COPY ./000-default.conf /etc/apache2/sites-available/000-default.conf
COPY ./apache2.conf /etc/apache2/apache2.conf
COPY ./public_html /var/www/html

WORKDIR /var/www/html

RUN a2enmod rewrite

EXPOSE 80

```

* 000-default.conf
* apache2.conf
* public_html (WordPressのファイル群)


#### Cloud Runの設定解説

ボリュームのマウント

2024年春頃にGAしたCloudRunのボリュームマウントがCloud Storageの静的ウェブサイト ホスティング機能を利用できたので採用しました。

公式の記事
https://cloud.google.com/blog/ja/products/serverless/introducing-cloud-run-volume-mounts

![スクリーンショット 2024-08-12 12.48.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985033/2faaf5ea-0ef5-1939-6411-59ecb83b1183.png)

yaml
```
        volumeMounts:
        - name: wp-media
          mountPath: /var/www/html
        startupProbe:
          timeoutSeconds: 240
          periodSeconds: 240
          failureThreshold: 1
          tcpSocket:
            port: 80
      volumes:
      - name: wp-media
        csi:
          driver: gcsfuse.run.googleapis.com
          volumeAttributes:
            bucketName: wp-media
```

ドメインマッピング

マッピングを追加でTXTレコード追加用のリンクを取得
![スクリーンショット 2024-08-11 16.03.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985033/a92a8595-98ae-1d5c-eeb8-ad2434040911.png)

XドメインのDNSに設定します
![スクリーンショット 2024-08-11 16.03.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985033/f26b60af-dc14-611f-8cca-68cbfef2a651.png)




#### CloudSQLにDBをインポートする

1️️. 移行用のCloud SQLを作成する
2. MySQLのDBをインポートする

<構成>

極力課金を抑える構成として、以下のようなスペックにしました

リージョン：asia-east1 (台湾)
ゾーン：シングルゾーン
マシン構成： 共有コア・1vCPU, 0.614GB
ストレージ：10GB
接続：パブリックIP
料金：$0.01/

※Cloud SQLがGoogle Cloudのリソースでランニングコストが高く、最安の構成でも1日約50円(月額1,500円)ほどかかります。この料金がレンタルサーバーを借りるより高くなってしまったので、移行できても維持せず閉鎖を決めました。

![スクリーンショット 2024-08-12 12.26.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985033/7b7b1957-b5c1-38f1-7a04-d81831a19072.png)

Cloud SQLに接続してMySQLのWordPress用SQLをインポートします

```
# cloud_sql_proxyを使ってコネクション作成
./cloud_sql_proxy -instances=****:asia-east1:wp-db=tcp:3306
2024/08/12 11:06:03 current FDs rlimit set to 61440, wanted limit is 8500. Nothing to do here.
2024/08/12 11:06:07 Listening on 127.0.0.1:3306 for learngcp-335008:asia-east1:wp-db
2024/08/12 11:06:07 Ready for new connections
2024/08/12 11:06:08 Generated RSA key in 242.316875ms
2024/08/12 11:06:44 New connection for "****:asia-east1:wp-db"
2024/08/12 11:06:44 refreshing ephemeral certificate for instance ****:asia-east1:wp-db
2024/08/12 11:06:45 Scheduling refresh of ephemeral certificate in 55m0s
2024/08/12 11:06:46 New connection for "****:asia-east1:wp-db"
```

MySQLの操作 (DBをインポート)
```
mysql -u ***_wp1 -p -h 127.0.0.1                                                                       
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 602859
Server version: 8.0.31-google (Google)

Copyright (c) 2000, 2024, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| ***_wp1         |
| mysql              |
| performance_schema |
| sys                |
+--------------------+

mysql -u[ユーザー名] -p[パスワード] [インポートするデータベース名] < [インポートするファイル名]
```

#### 移行した個人ブログ

ぼかしをかけさせてもらいますが、無事移行できた状態です

![スクリーンショット 2024-08-12 13.13.10.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985033/ee3b2139-a757-6dee-d9c9-79b9af14d29b.jpeg)

CloudRunに移行した欠点としては、やはりコールドスタートによる初回アクセス時のレイテンシーが遅い点です。(数秒レベル) Allways on CPUなどの設定次第で30ms以下にはできそうですが、ランニングコストがかなり高くなるので、許容金額を確認して設定することをお勧めします。


#### やってみた感想

Cloud Runを使って十分にWordPrssを安価・手軽に利用できるようになり、さらに使い勝手が良いサービスになった印象です。しかしながら、レンタルサーバーでの運用で問題なければCloud Runに移行せずとも良いと感じました。(コールドスタートのレイテンシー、Cloud SQLの運用コストなどがネック)

手順を調べて休日の空き時間等で進めてみたものの、所々でエラーが出ては解消しての繰り返し対応がそこそこ発生して想定より手こずりました・・ ChatGPTやClaudeなどの生成AIのおかげで、IaCのterraformコード作成はかなり時短できた印象です。

普段WordPressを利用していて、コンテナ化したサーバレス環境で運用してみたい方はぜひお試しください。