GCPを1ミリも知らなかった新卒が業務で学んだ知識でデプロイできるようになるまで
はじめに
オールアバウトグループの新卒1年目エンジニアが投稿する企画『テックブログ新卒週間2023』2日目の投稿です。
オールアバウトでPrimeAd関連サービスの開発をしているhinakochiです。 サービスの一つのリビルドに関わる中で、アプリをGKEでデプロイする経験をしました。入社するまでGCP自体ほぼ触ったことがなかったので、当時はついていくので精一杯になってしまいました。改めて何が起こっていたのかを理解するため、一度最初から最後までを自分でやりきりたいと思いました。約3ヶ月間、主に平日に一日一時間程度を使って簡単なアプリのデプロイに挑戦してきたので、これを機にその過程と学んだことをまとめてみました。遠回りが多くできたことはごくわずかですが、少しでもどなたかの参考になれば幸いです。
使用したもの
コンテナとアプリ構成: php-fpmコンテナ(appサーバ) + nginxコンテナ (webサーバ) + CloudSQL (dbサーバ)
Laravelのappコンテナとリバースプロキシ用のnginxのwebコンテナを自作イメージを使って立てます。これは業務で関わったものとほぼ同じ構成です。データベースには、本番ではCloud SQL Auth Proxyで接続します(ローカルではMySQLコンテナを立てていました)。 今回はデータベースに保存したテキストを取得して表示する簡単なテストページを用意しました。外部IPアドレスに接続してこれが表示できればゴールとします。
最終的な構成のイメージはこのようになります。
GCPで使用した主なサービス
Kubenetes Engine
コンテナの自動管理ツールです。コンテナの集合体の最小単位をポッド、ポッドを実行するマシンをノード、ノード群をクラスターと呼びます。マニフェストファイルでポッドの構成やスケーリングの条件などを定義しておき、リクエストに応じて自動的にスケールさせることができます。
Cloud SQL
クラウドベースのリレーショナルデータベースシステムです。
Container Registory
コンテナイメージを保存・管理できるサービス。ただしイメージのストレージ自体はCloud Storageが担っています。(現在はOSパッケージなども保存できるよう拡張されたArtifact Registoryが推奨されています。参考:Container Registry のドキュメント)
IAM
サービスというより、GCPの各サービスを使用するためのアカウント体系です。個人、組織、サービスなどの固有のアドレスをプリンシパルという概念で並列に扱い、様々な権限を付与します。個人的にはここで一番よく引っかかりました。具体的には、足りない権限が何か、それをどう与えるのかがわからない、IAMのサービスアカウントとKubernetesなどサービス特有のアカウントとの関係がわからないなどの点で悩みました。
ちなみに90日間30$分の無料トライアルを利用しました。実は間に合わず期限後少しだけアップグレードすることになったのですが、クレジットが残っている分は引き継げるので大した負担にならなくてよかったです。
その他
GitHub Actions
CI/CDツール。コードもGitHubで管理しています。
Kustomize
Kubernetesのマニフェストファイルを複数まとめて管理できるツールです。参考にしたチュートリアルに登場したので試しに触ってみました。今回はデプロイ環境を一種類しか用意していないのでそこまでメリットはないのですが、ベースのマニフェストファイルを動的に書き換えて使う方法を学べました。
やったこと
実際の手順を辿りながらハマった部分をピックアップしていきます。ローカルで動くアプリは一旦完成している状態です。
デプロイ前のGCP側の準備
プロジェクト作成
ブラウザの管理コンソールで作成しました。 プロジェクトはKubernetesに限らずGCP内の様々なサービスに跨がります。
API有効化(Container Registory, Kubernetes Engine)
gcloud services enable \ containerregistry.googleapis.com \ container.googleapis.com
これで該当サービスが使えるようになります。 この時点でデータベースの実現方法は一旦置いていたのでCloud SQL周りはまだ何もしていません。 このあたりの工程から下記の記事を参考にさせていただきました。
GitHub Actions を使用してGKEにアプリケーションをデプロイする
Kubernetesクラスタ作成
Kubernetesクラスタは作成の際、標準モードとAutopilotモードを選択できます。Autopilotモードは設定しなくとも自動スケーリングやノード管理をある程度やってくれるようです。せっかくなので使ってみましたが、小規模なアプリなので特に恩恵はなかったと思われます。
クラスタ作成の過程で誤って飛ばしてしまい、その結果長時間悩むことになったステップを紹介します。
# クラスタの認証情報を取得する gcloud container clusters get-credentials sample-cluster-name \ --zone zone-name --project sample-project-name
KubernetesのCLIツールであるkubectlを使用するにはクラスタを選択して接続する必要があります。 この設定ができていないうちは下記のようなエラーが出ており、次のセクションで出てくるような個別のロールの設定が必要なのかとしばらく誤解していました。
Error from server (Forbidden): deployments.apps "deploy-***" is forbidden: User "***" cannot get resource "deployments" in API group "apps" in the namespace "default": requires one of ["container.deployments.get"] permission(s).
サービスアカウントの設定
# サービスアカウント新規作成 gcloud iam service-accounts create sample-project-service-account # プロジェクトに対してサービスアカウントとロールを紐づける gcloud projects add-iam-policy-binding sample-project-name \ --member=serviceAccount:sample-project-service-account@sample-project-name.iam.gserviceaccount.com \ --role=roles/container.admin --role=roles/storage.admin # サービスアカウントキーを作成&出力 gcloud iam service-accounts keys create key.json \ --iam-account=sample-project-service-account@sample-project-name.iam.gserviceaccount.com cat key.json | base64
サービスアカウントは各サービスを利用するためのアカウントです。サービスアカウントにプロジェクトにおけるロールを与えると、ロールによってできることが追加されます。container.adminはKubernetes Engineのオブジェクト全般に対する管理者、storage.adminはCloud Storageに対する管理者ロールです。Cloud Storageはプロジェクト内のオブジェクトを保持するストレージサービスなのでこちらの権限も必要になります。 出力したサービスアカウントキーはGitHub Actions側でsecretsに登録し、Actionsから操作できるようにします。 ※今回は個人的な学習目的の環境なので大きな問題はありませんでしたが、最近はサービスアカウントキーをCI/CDツールに登録することのリスクが指摘されています。GitHub Actions OIDCや後述のWorkload Identityなどを利用して直接サービスアカウントキーを登録せずに運用することができます。弊社がGitHubで管理しているサービスはGitHub Actions OIDCを利用しています。
appコンテナとwebコンテナのデプロイ
Kustomizeの設定
ファイル構成は以下の通りです。
deploy/ - kustomization.yaml - deployment.yaml - service.yaml
kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - manifest.yaml - service.yaml images: - name: web-image - name: app-image
# kustomization.yamlのあるディレクトリに移動 cd deploy # app-imageというイメージ名変数に使用したいイメージ名を設定 kustomize edit set image app-image=gcr.io/$PROJECT_ID/$IMAGE-app:latest # web-image kustomize edit set image web-image=gcr.io/$PROJECT_ID/$IMAGE-web:latest # deployment等を作成してそれをもとにリソースを更新 kustomize build . | kubectl apply -f
kustomize edit set imageでkustomization.yamlで用意した変数にイメージ名を設定し、変数が置き換わった状態でbuildすることができます。
deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: sample-project labels: app: app-name spec: replicas: selector: matchLabels: app: app-name template: metadata: labels: app: app-name spec: serviceAccountName: sample-ksa containers: - name: web image: web-image # kustomize edit set imageで更新 ports: - containerPort: 81 volumeMounts: - mountPath: /var/run/php-fpm name: nginx-sock - name: app image: app-image # kustomize edit set imageで更新 volumeMounts: - mountPath: /var/run/php-fpm name: nginx-sock # 後略
ちなみに、次のコマンドもbuildができるのですが、buildした結果のdeploymentが出力されるのでデバッグに役立ちました。
oc kustomize
正しいイメージを使用できているかの確認
デプロイが成功したらまずこれが大事だと振り返ってみて思います。私の場合は、Podが動いていなくてPodの設定まわりを確認するのに時間を費やしてしまったのですが、実はイメージがpushできていない、イメージを指定できていない、古いイメージを使ってしまっているなどのミスがありました。 前セクションのocコマンドで最新のdeploymentを確認できるほか、ターミナルから次のコマンドで実際のPodで使用されているイメージを含む様々な情報を確認できます。
kubectl describe pods pod-name
コンテナ間通信
今回はUnixドメインソケットを使用しています。Unixドメインソケットを用いた通信はTCP通信より高速で、同じマシン内であればデータ通信用のファイルパスを通すことで通信できます(今回はappコンテナとwebコンテナが同一のマシンに乗っている状態なのでこの方法が可能です)。GCPとは話がずれますが、こちらもうまく接続できるまで非常に時間がかかった部分なので、どなたかの参考になることを願い簡潔に残しておきます。
deployment.yaml
apiVersion: apps/v1 kind: Deployment # 中略 containers: - name: web image: web-image ports: - containerPort: 81 volumeMounts: - mountPath: /var/run/php-fpm name: nginx-sock - name: app image: app-image volumeMounts: - mountPath: /var/run/php-fpm name: nginx-sock # 中略 volumes: - name: nginx-sock emptyDir: {}
上記の設定では、nginx-sockというボリュームを確保し、コンテナごとに設定したパスでマウントします。 次はnginx側の設定です。
web/default.conf
server { listen 81; # webコンテナ containerPortの数字 server_name example.com; root /data/public; # 中略 location ~ \.php$ { fastcgi_pass unix:/var/run/php-fpm/nginx.sock; # ソケットファイルパス fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 中略 }
appコンテナ側では、ソケットファイルにアクセス可能なシステムユーザーを用意する必要があります。
app/www.conf
[www] listen = /var/run/php-fpm/nginx.sock # ソケットファイルパス user = nginx # ユーザー設定 group = nginx listen.owner = nginx # listenに使用するユーザーを設定 listen.group = nginx listen.mode = 0660 # 後略
app/dockerfile
FROM php:8.2-fpm ENV TZ Asia/Tokyo RUN apt-get update && \ apt-get install -y git unzip libzip-dev libicu-dev libonig-dev && \ docker-php-ext-install intl pdo_mysql zip bcmath # ユーザーを作成する RUN addgroup --system nginx && adduser --system --ingroup nginx nginx COPY ./docker/app/php.ini /usr/local/etc/php/php.ini # confファイルを配置 COPY ./docker/app/www.conf /usr/local/etc/php-fpm.d/zz-www.conf COPY . /data RUN chmod 777 /data/storage -R COPY --from=composer:2.0 /usr/bin/composer /usr/bin/composer WORKDIR /data
confファイルをzz-www.confという名前に変更していることに注意してください。ここで使用している公式イメージでは、ビルド時にデフォルトのwww.confで上書きをしてしまう仕様があるようです。辞書順で後ろになるファイルを用意すればその内容が優先になるため、名前を変えて対応できるということのようです。 参考: PHPの公式DockerイメージでUNIXソケット通信しようとして罠にハマる
外部アクセス
service.yaml
apiVersion: v1 kind: Service metadata: name: sample-project-service labels: app: app-name spec: type: NodePort ports: - port: 81 nodePort: 30080 selector: app: app-name
次のコマンドで得られる外部IPとnodePortの値をコロンで繋いで接続できます。
kubectl get nodes -o wide
Cloud SQLの初期設定
API有効化
gcloud services enable sqladmin.googleapis.com
Cloud SQLのインスタンス作成
これだけ管理コンソールで作成しました。MySQLを選択したほかはデフォルト設定です。 データベースはLaravel側でマイグレーションファイルを用意しています。
Cloud SQLのユーザー作成
gcloud sql users create sample-db-user \ --instance=sample-db-instance --password=sample-db-password
Cloud SQLの接続設定
サービスアカウントにロール追加
# Kubernetes Engine管理者ロールのあるサービスアカウントにCloud SQL接続クライアントのロールを追加 gcloud projects add-iam-policy-binding sample-project-name \ --member=serviceAccount:sample-project-service-account@sample-project-name.iam.gserviceaccount.com \ --role=roles/cloudsql.client
ここからのステップは以下の公式ドキュメントの「Secret オブジェクトを作成する」「Cloud SQL Auth Proxy にサービス アカウントを提供する > Workload Identity」を参考にしています。
Google Kubernetes Engine から Cloud SQL への接続について
Cloud SQL Auth ProxyとはIAMを用いた認証でCloud SQLに接続するためのコネクタです。今回はこのコネクタの公式イメージを使用してPodにコンテナを生成して接続します。
Secret オブジェクトを作成する
Kubernetesで使えるkey-value型のオブジェクトを作成します。後ほどマニフェストに使用します。
kubectl create secret generic sample-db-secret \ --from-literal=username=sample-db-user \ --from-literal=password=sample-db-password \ --from-literal=database=sample-db-name
Workload Identityを有効化する
Workload Identityとは、Kubernetesのクラスタ内のサービスアカウントにIAMで作成したサービスアカウントを紐づけて接続する仕組みです。認証回数が減る、サービスアカウントキーを保持する必要がなくなる、アクセス範囲を狭められるなどのメリットがあります。appコンテナとwebコンテナはまさにサービスアカウントキーを使用してしまったので、Workload Identityを用いた改善の余地があります。こちらは今後の課題とします。
Autopilotモードでクラスタを作成すると、デフォルトで有効になっています。
Kubernetesのサービスアカウントを作成する
kubernetes-service-account.yaml
apiVersion: v1 kind: ServiceAccount metadata: name: sample-ksa
deploymentなどと同様にマニフェストファイルを作成し、applyすることで作成されます。
サービスアカウント同士を紐づける
# KubernetesのサービスアカウントにWorkload Identityのユーザーとしてのロールを付与 # それをプロジェクトのサービスアカウントがバインドする gcloud iam service-accounts add-iam-policy-binding \ --role="roles/iam.workloadIdentityUser" \ --member="serviceAccount:sample-project.svc.id.goog[sample-namespace/sample-ksa]" \ sample-project-service-account@sample-project-name.iam.gserviceaccount.com # Kubernetesのサービスアカウントに、プロジェクトのサービスアカウントがどれかをアノテーションする kubectl annotate serviceaccount sample-ksa \ iam.gke.io/gcp-service-account=sample-project-service-account@sample-project-name.iam.gserviceaccount.com
これによりプロジェクトのサービスアカウントが様々なサービスに認証なしでアクセスできるようになります。 アノテーションに間違いがあった場合、--overwriteオプションをつけて再実行することで更新できます。
Cloud SQLコネクタの設定
deployment.yaml
apiVersion: apps/v1 kind: Deployment # 中略 spec: serviceAccountName: sample-ksa containers: - name: web # 中略 - name: app # 中略 env: # appコンテナの環境変数を設定 - name: DB_USERNAME valueFrom: secretKeyRef: name: sample-db-secret key: username - name: DB_PASSWORD valueFrom: secretKeyRef: name: sample-db-secret key: password - name: DB_DATABASE valueFrom: secretKeyRef: name: sample-db-secret key: database - name: cloud-sql-proxy image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.1.0 args: - "--structured-logs" - "--port=3306" - "sample-project-name:zone-name:sample-db-instance" securityContext: runAsNonRoot: true resources: requests: memory: "2Gi" cpu: "1" volumes: - name: nginx-sock emptyDir: {}
KubernetesのSecretに登録した値をappコンテナの環境変数に設定します。Laravelの.envに同名の環境変数があれば、それを上書きします(つまり.env側の記述が不要になります)。 cloud-sql-proxyのargsの3番目に入る値は、Cloud SQLのコンソールの概要 > lこのインスタンスとの接続 > 接続名 から取得できます。
データベースに接続する
gcloud sql connect sample-db-instance --user=sample-db-user
これで直接データベースを操作できます。appコンテナに入ってマイグレーションを実行してから接続すれば、テーブルができています。
終わりに
GCPに関してほぼ無知な状態から、業務で触れる中で多少は理解できてきたと思っていました。しかし、自分で全てをやってみるうちに、氷山の一角でしかないという実感が強くなる一方でした。本当はもっと業務で触れなかったサービスも使ってみようと思っていたのに既知サービスの時点で難航したこと、Workload Identityの件に気づくのが遅すぎて手が回らなかったことなど、悔しい面はたくさんありますが、自分の力量を知る良い機会になりましたし、曲がりなりにもデプロイに漕ぎ着けたことはある程度自信になったと思います。今後もGCPに限らず実践的な学習を続けていき、業務に活かしていきたいです。
参考資料
GitHub Actions を使用してGKEにアプリケーションをデプロイする
PHPの公式DockerイメージでUNIXソケット通信しようとして罠にハマる