Twitterクローンを作ります #18 CI/CD


本当はもっと早くやっておきたかったのですが、このあたりでCI/CDの設定をしておきましょう。

CI/CDのサービスとして、GitHub Actionsを利用しましす。
GCPへのデプロイでは、Workload Identity 連携を利用すると便利そうなので使ってみます。

https://cloud.google.com/iam/docs/configuring-workload-identity-federation#github-actions

# GCP側の設定

はじめに、デプロイに利用するサービスアカウントを作成します。

GCP管理画面から "IAM と管理" > "サービスアカウント" を選択します。

"+ サービスアカウントを作成" をクリックします。

"サービスアカウント名" と "サービスアカウントID" は "ci-deploy" としておきます。

ロールは "Cloud Run デベロッパー" と "Firebase Hosting 管理者" を割り当てておきます。



GCP管理画面から "IAM と管理" > "Workload Identity 連携" を選択します。

IDプールを作成します。
名前を `dev-pool` としました。 

"プロバイダの選択" で "OpenID Connect (OIDC)" を選択します。

"プロバイダ名" を "GitHub"
"プロバイダID" を "gh-provider"
"発行元 (URL)" を "https://token.actions.githubusercontent.com"
"オーディエンス" は "Default audience" 
に設定します。
Default audience に表示されている "https://iam.googleapis.com/projects/....." の URL をメモしておいてください。

"プロバイダの属性を構成する" では下記の様に設定します。

Google 1 : google.subject
OIDC 1 : assertion.sub
Google 2 : attribute.repository
OIDC 2 : assertion.repository

保存します。


"+ アクセスを許可" をクリック

サービスアカウントは ci-deploy
プリンシパルの選択は "フィルタに一致するIDのみ" で repository, apollo-devel/toitta に設定します。
(apollo-devel/toitta は自身のGitHubアカウントとリポジトリに書き換えてください)


# CI/CDの設定

今度はCI/CDの設定を行っていきます。

MongoDBの認証情報が結構ややこしいのですが、
 GitHub Secrets に MongoDBの認証情報を設定しておく
 → deploy.yaml で Secrets にアクセスできるので、これを gcloud builds submit の substitutions 機能で cloudbuild.yaml にわたす
 → cloudbuild.yaml から Dockerイメージのビルドの引数 --build-arg にわたす
 → Dockerfile 内で環境変数に設定される
という受け渡しが必要です。

まずは GitHub の secrets に保存しておきます。

GitHub のリポジトリ画面から "Settings" > "Secrets" > "Actions" を選択します。
"New Repository Secret" をクリックします。
名前を"MONGO_CREDENTIAL"とし、値に"admin:XXXXXX" (正しいユーザー名とパスワードに変更してください) に設定して保存します。


deploy.yaml を以下のように作成します。

```yaml:.github/workflows/deploy.yaml
name: Deploy

on:
  push:
    branches: master

permissions:
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v0'
        with:
          workload_identity_provider: 'projects/837189962146/locations/global/workloadIdentityPools/dev-pool/providers/gh-provider'
          service_account: 'ci-deploy@toitta-dev.iam.gserviceaccount.com'
      
      - uses: 'actions/setup-node@v2'
        with:
          node-version: '14'

      - name: 'Build Client'
        working-directory: ./client
        run: |-
          npm install
          npm run build        

      - name: 'Build Image'
        working-directory: ./server
        run: gcloud builds submit --substitutions=_CRED=${{ secrets.MONGO_CREDENTIAL }}

      - name: 'Deploy API'
        uses: 'google-github-actions/deploy-cloudrun@v0'
        with:
          service: 'toitta-api'
          image: 'gcr.io/toitta-dev/toitta-api'
          region: 'asia-northeast1'

      - name: 'Deploy Client'
        working-directory: ./client
        run: |-
          npm install -g firebase-tools
          firebase deploy

```

さらにcloudbuild.yaml を以下のように作成します。

```yaml:server/cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/docker'
  args: ['build', '-t', 'gcr.io/toitta-dev/toitta-api', '--build-arg=CRED=${_CRED}', '.']
images: ['gcr.io/toitta-dev/toitta-api']
serviceAccount: 'projects/toitta-dev/serviceAccounts/ci-deploy@toitta-dev.iam.gserviceaccount.com'
substitutions:
 _CRED: DUMMY
options:
 logging: CLOUD_LOGGING_ONLY
```

最後にDockerfileを以下のように修正します

```Dockerfile:server/Dockerfile
# Use the official lightweight Python image.
# https://hub.docker.com/_/python
FROM python:3.10-slim

# Allow statements and log messages to immediately appear in the Knative logs
ENV PYTHONUNBUFFERED True

# TODO
ARG CRED
ENV MONGO_CREDENTIAL ${CRED}

# Copy local code to the container image.
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . ./

# Install production dependencies.
RUN pip install --no-cache-dir -r requirements.txt -c constraints.txt

# Run the web service on container startup. Here we use the gunicorn
# webserver, with one worker process and 8 threads.
# For environments with multiple CPU cores, increase the number of workers
# to be equal to the cores available.
# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling.
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
```

あー面倒臭いですね!

# 問題の修正

一応これでデプロイできて、画面も表示できてログインAPIもレスポンスはちゃんと返してくれているので動いてはいるようなのですが、つぶやきの取得で401になってしまいました。
調べてみると、Cookie名は `__session` にして置かなければならないようです...

https://firebase.google.com/docs/hosting/manage-cache?hl=ja#using_cookies

ということで修正しましょう。

```python:server/main.py
from flask import Flask
import logging

app = Flask(__name__)
app.secret_key = 'DUMMY'
app.config['SESSION_COOKIE_NAME'] = '__session'
app.logger.setLevel(logging.INFO)


from apis import login, users, posts
```

今回はこの程度の修正で済んだのであまり問題ないですが、やはり早い段階でデプロイまで含めた仕組みを作っておいたほうがいいですね。

![スクリーンショット 2022-03-18 18.05.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2536525/c86a783a-94ae-6f7a-93fc-596d99ec8146.png)

これでpushするたびにデプロイできるようになりました。
次回はlintと単体テストの実行あたりをやっていこうと思います。