🦁

GitHub ActionsとCloud RunでZennの記事を限定公開する

2022/05/24に公開約12,000字

はじめに

こんにちは、M-Yamashitaです。

今回の記事は、GitHub Actionsを使用してZennの記事を限定公開としてデプロイする話です。
以前「Fukuoka.rb 0x100 回 LT 大会 (#256)」で登壇した内容を、記事として起こしました。
この話では、スライドに加えて、登壇で話せなかったGCPの権限周りの話や作成したワークフローファイルについても記載しています。

https://fukuokarb.connpass.com/event/245647/

https://speakerdeck.com/myamashii/github-actionsdezennfalseji-shi-woxian-ding-gong-kai-suru

この記事で伝えたいこと

  • 限定公開のワークフローを作るまでの背景
  • ワークフローを使用するための準備
  • 作成したワークフローの紹介

きっかけ

以前から技術記事を投稿しており、その技術記事の執筆ではQiitaやZennを使用していました。今は主にZennを使用しています。
Zennを使って執筆した記事のほとんどは、公式サイトや公開されている情報を個人で調査してまとめていた記事でした。そのため、公開前に特定の人だけに見せる事はありませんでした。

そんな中、以下記事を執筆している最中、公式のリファレンスに書いてある仕様について、リファレンスの文章を読んでも実際に動かしてもよく分からない問題に出会いました。

https://zenn.dev/m_yamashii/articles/subquery_optimization

この仕様についてコミュニティで質問したところ、具体例や仕様の説明について回答を頂くことができ、記事を作成することができました。
この記事は回答していただいた方がいらっしゃったことで完成できたので、その方に記事への名前の掲載可否を尋ねようと思いました。また、記事を見せてほしいと伝えられることもあると思い、間違い等ないか確認していただくために、限定公開としてプレビューをすぐ出せるようにしたいと考えました。

ただ、Zennには限定公開の機能がないようでした(本記事執筆時点でもその機能はないようです)。そのため、似た事を考えた人がいるはずと思い調べたところ、「Zennの記事を限定公開する方法」という記事がヒットしました。

https://zenn.dev/e_koma/articles/20210104-zenn-preview

この記事によると、ZennにはCLIがありnpx zenn previewでプレビューが表示できるため、それを活用することで限定公開ができるとありました。

https://zenn.dev/zenn/articles/zenn-cli-guide

紹介されていた限定公開の全体図:

限定公開の全体図

記事で紹介されていた限定公開の手順は以下のとおりです。
まずはZenn CLIを使って記事を作成します

Zenn CLIをinstallしてnpx zenn new:article を実行すると、articles配下に記事の雛形ファイルができるので、このファイルに記事を書きます。

.
└─ articles
   └── example-article1.md

次にDockerfileを作成します。

FROM node:lts-alpine3.12

WORKDIR /app
RUN apk add --no-cache --virtual .build-deps git \
    && npm init --yes \
    && npm install zenn-cli \
    && npx zenn init \
    && apk del .build-deps
COPY articles articles
COPY books books

ENTRYPOINT ["npx", "zenn", "preview"]

Dockerfileを作成したら、指定のディレクトリにそのDockerfileを配置します。

記事のリポジトリのrootにDockerfileを起きます

.
├─ articles
│  └── example-article1.md
└─ Dockerfile

このディレクトリ上でgcloudコマンドを使用し、Container Registryにイメージをプッシュし、Cloud Runにデプロイすることで、Cloud Run上でZennのプレビューを見ることができる専用のURLが生成されます。

この方法により、当初抱えていた

記事を見せてほしいと伝えられることもあると思い、間違い等ないか確認していただくために、限定公開としてプレビューをすぐ出せるようにしたい

が解決できました。

課題

この限定公開を何度か行っているうち、以下3つの課題が出てきました。

  • 複数のコマンドを手動実行するのは手間がかかる
  • ZennとGitHubを連携済みなので記事作成のPull Requestをトリガーにプレビューを見たい
  • プレビューに使ったCloud Runのサービスやコンテナイメージの消し忘れを避けたい

複数のコマンドを手動実行するのは手間がかかる

限定公開のためには以下のコマンドを順番に実行する必要があります。

  • 指定したプロジェクト名に対して実行するようにgcloudコマンドのコンフィグを設定
  • gcloudコマンドでdockerを使用できるようにする
  • プロジェクト名を含む特定のタグでコンテナーのビルド、プッシュ
  • 上記タグとデプロイ用のサービス名を指定しgcloudコマンドでデプロイ

この実行において、プロジェクト名やタグ名はタイポする可能性があります。また時間が経ってしまうとそれらの名前やコマンド自体を忘れてしまうことが考えられます。そのたびに、毎回調べたり思い出しながら実行したりするのは大変です。

ZennとGitHubを連携済みなので記事作成のPull Requestをトリガーにプレビューを見たい

私の環境では、Zenn CLIをインストールし、かつZennとGitHubを連携した環境を構築していました。
Zennには、記事内のpublishedtrueにして、GitHubの特定のブランチにpushすると、自動でZennに投稿する仕組みがあります。

https://zenn.dev/zenn/articles/connect-to-github
  1. 同期するブランチ名を確認
    デプロイページで同期したいブランチ名を確認・変更します。
    ここで登録されている名前のブランチに変更があったときに自動でデプロイが行われます。

https://zenn.dev/zenn/articles/zenn-cli-guide

記事を zenn.dev 上で公開するにはpublishedオプションがtrueになっていることを確認したうえで、ファイルをコミットし、Zenn と連携されている GitHub リポジトリにプッシュします。
Zenn と連携したリポジトリの登録ブランチにプッシュされると、同期(デプロイ)が開始されPます。

私の環境ではその特定のブランチをmainブランチとしているため、publishedtrueとしてmainブランチに誤ってpushしてしまうと、確認なしでZennに投稿されてしまいます。
そのため、mainブランチでの作業を行わず、別ブランチで作業しマージする事を考えました。
この方法は、OSSでのPull RequestによるContributionsと似ていると思い、その流れに沿って、Zennの記事を新しく作成したらPull Requestを出すようにしました。

Pull Requestを作成した際、Cloud Runへのデプロイが自動で実行されたら嬉しいなと思っていました。その際問題になったのは、デプロイで生成される限定公開のURLをどこに表示するかです。
デプロイをするとそのコンソール上で限定公開のURLが表示されるので、コンソールを見に行く方法を検討してみました。ですが、毎回コンソール上のURLを見に行ったり、URLをコピペしてブラウザに貼り付けるのは手間がかかります。

そのためデプロイが完了したら、Pull Requestの画面に限定公開のURLを表示したいと思いました。これにより、ワンクリックで限定公開を見ることができ、とても楽になると思いました。

プレビューに使ったCloud Runのサービスやコンテナイメージの消し忘れを避けたい

ローカルPC上でgcloudコマンドを使用してデプロイした際、何度かCloud RunのサービスやContainer Registryのイメージを消し忘れることがありました。
Cloud Runには無料枠があったり、Container Registryは重いイメージでなければ料金は微々たる範囲に収まったりしますが、不要なものをそのままにしていたために料金を請求されることは避けたいと思いました。
またセキュリティの面でも、不要なものをそのままにしておくのはあまりよろしくありません。

https://cloud.google.com/run/pricing?hl=ja
https://cloud.google.com/container-registry/pricing

そのため、限定公開での確認が終わったら自動的に削除する仕組みがほしいと思いました。

課題の解決

さきほどの課題3つを解決するために、GitHub Actionsを使用することにしました。

  • 課題に対する解決方法
    • 複数のコマンドを手動実行するのは手間がかかる
      → GitHub Actionsのワークフローに必要なコマンドを書いておく
    • ZennとGitHubを連携済みなので記事作成のPull Requestをトリガーにプレビューを見たい
      → Pull Requestが作成されたり更新された場合にGitHub Actionsを実行し、デプロイしたCloud RunのURLを、botを使ってPull Requestにコメントで投稿する
    • プレビューに使ったCloud Runのサービスやコンテナイメージの消し忘れを避けたい
      → Pull Requestのイベントが起きるたびに毎回Cloud Runのサービスやイメージを削除する

なお、なぜGitHub Actionsを採用したのか?Circle CIなどの選択肢もあったのでは?については、仕事やプライベートで使っていて知見があることや、私自身がGitHub Actionsを使いたかったからです。

全体構成

構成は以下の通りです。

構成図

この構成上で、GitHub Actionsのワークフローで行うことは以下のとおりです。

  • プレビュー表示ジョブ
    (実行タイミング: Pull Requestのopen、reopen、pushによる更新発生時)
    • 既存のCloud Runのサービスを削除
    • 既存のコンテナイメージを削除
    • Container Registryへイメージプッシュ
    • Cloud Runにデプロイ
    • デプロイ完了後にプレビューのURLをPull Requestに投稿
  • プレビュークローズジョブ
    (実行タイミング: Pull Requestのclose時)
    • 既存のCloud Runのサービスを削除
    • 既存のコンテナイメージを削除

ワークフロー作成前の準備

Secretsの設定

ワークフローではGCPのプロジェクトIDと、GitHub ActionsからGoogleCloudを使用するためのサービスアカウントを使用します。
そのためこれらを取得し、リポジトリのSecretsに保存します。

プロジェクトIDの取得

GCP上でプロジェクトを作成し、プロジェクトIDを取得します。
プロジェクト作成、プロジェクトIDの取得に関しては以下公式ドキュメントを参照してください。

https://cloud.google.com/resource-manager/docs/creating-managing-projects

取得したプロジェクトIDをPROJECT_IDとして、リポジトリのSecretsに保存します。

GitHub ActionsからGoogleCloudと認証するためのサービスアカウント取得

GitHub ActionsからGoogle Cloudとの認証を行うため、ワークフローではgoogle-github-actions/authのアクションを使用しています。

https://github.com/google-github-actions/auth

このアクションでは認証のための連携方法を選べるので、今回はWorkload Identity Federationを採用しました。
Workload Identity Federationを使用するための事前準備は、以下クラスメソッドさんの記事や、google-github-actions/authアクションの「Setting up Workload Identity Federation」の手順を基に実施しました。

https://dev.classmethod.jp/articles/google-cloud-auth-with-workload-identity/
https://github.com/google-github-actions/auth#setup

上記手順を行い、gcloud iam workload-identity-pools providers describe・・・のコマンドまで実行すると、Workload Identityプロバイダーの名前を取得できます。
この名前をWORKLOAD_IDENTITY_PROVIDERとして、リポジトリのSecretsに保存します。

作成したサービスアカウントに対してロールを追加する

「GitHub ActionsからGoogleCloudと認証するためのサービスアカウント取得」項目で作成したサービスアカウントの権限だけでは、Container RegistryへのプッシュやCloud Runへのデプロイができません。
そのためロールを追加します。

  • Container Registryを使用するためのロール追加

    • Container Registryへのpushに関しては、公式にて必要な権限とロールが記載してあります。このリファレンスを基に、作成したサービスアカウントにストレージ管理者(roles/storage.admin)のロールを紐付けます。
      https://cloud.google.com/container-registry/docs/access-control

      Note: Pushing images requires object read and write permissions as well as the storage.buckets.get permission.

  • Cloud Runを使用するためのロール追加

    • Cloud Runへのデプロイも同様に、必要な権限とロールがあります。このリファレンスを基に、作成したサービスアカウントに、Cloud Run管理者(roles/run.admin)、サービス アカウント ユーザー(roles/iam.serviceAccountUser)のロールを紐付けます。
      https://cloud.google.com/run/docs/reference/iam/roles

      A user needs the following permissions to deploy new Cloud Run services or revisions:

      • run.services.create and run.services.update on the project level are required. run.services.get is not strictly required, but is recommended in order to read the status of the created service. Typically assigned through the roles/run.admin role. It can be changed in the project permissions admin page.
      • iam.serviceAccounts.actAs for the Cloud Run runtime service account. By default, this is PROJECT_NUMBER-compute@developer.gserviceaccount.com. The permission is typically assigned through the roles/iam.serviceAccountUser role.

これでロールの追加は完了です。

ワークフローファイル

作成したワークフローファイルはこちらになります。

zenn-preview.yml
name: zenn-preview

on:
  pull_request:
    branches: [ main ]
    types: [opened, synchronize, reopened, closed]

  workflow_dispatch:

jobs:
  display-preview:
    if: ${{ github.event.action != 'closed' }}
    runs-on: ubuntu-latest

    permissions:
      contents: 'read'
      id-token: 'write'
      pull-requests: 'write'

    steps:
      - uses: actions/checkout@v3

      - name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v0'
        with:
          workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
          service_account: 'my-service-account@${{ secrets.PROJECT_ID }}.iam.gserviceaccount.com'

      - name: 'Set up Cloud SDK'
        uses: 'google-github-actions/setup-gcloud@v0'

      - name: 'Delete services from Cloud Run'
        run: |
          gcloud run services list \
          | sed 1d \
          | awk '{print $2}' \
          | xargs -n 1 -I{} gcloud run services delete {} --platform managed --region asia-northeast1 --quiet

      - name: 'Delete images from Container Registry'
        run: |
          gcloud container images list-tags "gcr.io/${{ secrets.PROJECT_ID }}/zenn-preview" \
          | sed 1d \
          | awk '{print $1}' \
          | xargs -n 1 -I{} gcloud container images delete "gcr.io/${{ secrets.PROJECT_ID }}/zenn-preview@sha256:{}" --quiet --force-delete-tags

      - name: 'Configure docker for gcloud'
        run: gcloud auth configure-docker

      - name: 'Build image'
        run: docker build -t "gcr.io/${{ secrets.PROJECT_ID }}/zenn-preview" .

      - name: 'Push image'
        run: docker push "gcr.io/${{ secrets.PROJECT_ID }}/zenn-preview"

      - id: create_service_name
        name: 'Create service name'
        run: |
          SERVICE_NAME="zenn-preview-xxxxx"
          echo "::set-output name=SERVICE_NAME::${SERVICE_NAME}"

      - id: deploy
        name: 'Deploy to Cloud Run'
        uses: 'google-github-actions/deploy-cloudrun@v0'
        with:
          service: "${{ steps.create_service_name.outputs.SERVICE_NAME }}"
          image: "gcr.io/${{ secrets.PROJECT_ID }}/zenn-preview"
          region: 'asia-northeast1'
          flags: --allow-unauthenticated --port=8000

      - name: 'Post comments'
        uses: actions/github-script@v5
        with:
          script: |
            var preview_url_message = `Preview URL: ${{ steps.deploy.outputs.url }}\n`
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: preview_url_message
            })

  close-preview:
    if: ${{ github.event.action == 'closed' }}
    runs-on: ubuntu-latest

    permissions:
      contents: 'read'
      id-token: 'write'

    steps:
      - name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v0'
        with:
          workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
          service_account: 'my-service-account@${{ secrets.PROJECT_ID }}.iam.gserviceaccount.com'

      - name: 'Set up Cloud SDK'
        uses: 'google-github-actions/setup-gcloud@v0'

      - name: 'Delete services from Cloud Run'
        run: |
          gcloud run services list \
          | sed 1d \
          | awk '{print $2}' \
          | xargs -n 1 -I{} gcloud run services delete {} --platform managed --region asia-northeast1 --quiet

      - name: 'Delete images from Container Registry'
        run: |
          gcloud container images list-tags "gcr.io/${{ secrets.PROJECT_ID }}/zenn-preview" \
          | sed 1d \
          | awk '{print $1}' \
          | xargs -n 1 -I{} gcloud container images delete "gcr.io/${{ secrets.PROJECT_ID }}/zenn-preview@sha256:{}" --quiet --force-delete-tags

実行結果

Pull Requestを作成するとGitHub Actionsが実行され、botによってURLがPull Requestに投稿されます。
このURLをクリックすると、Cloud Run上で表示されたZennの記事を見ることができます。
実行結果

おわりに

この記事では、GitHub ActionsとCloud RunでZennの記事を限定公開する方法について説明しました。
ワークフロー内のコマンド作成やGCPの権限周りの理解が少し大変でしたが、今後記事を継続して投稿していく上でプレビューを必ず見るため、時間をかけた分の価値はあったと感じています。
この記事が誰かのお役に立てれば幸いです。

Discussion

ログインするとコメントできます