GAミント至上主義

Web Monomaniacal Developer.

Cloud BuildでApp EngineにデプロイしようとしたらPERMISSION_DENIED

NuxtJSで使った社内用アプリをCloud BuildでApp Engineに自動デプロイしようとしたら2時間くらいハマったのでメモ。

GitHub Actionsもいいけど、やっぱGCP内で完結させたいなぁということでCloud Build使いました。GitHub Actionsと比べるとユーザーと情報が少ないのが玉に瑕。

まずは公式参考にやる。
cloud.google.com

node_modulesディレクトリ等はレポジトリに含まれないため、installやビルドが必要。
cloudbuild.yamlはこんな感じ。
timeoutは公式そのまま。

steps:
- name: 'gcr.io/cloud-builders/yarn'
  args: ["install"]
- name: 'gcr.io/cloud-builders/yarn'
  args: ["nuxt-ts", "build"]
- name: "gcr.io/cloud-builders/gcloud"
  args: ["app", "deploy", "app.yaml", "-q", "--project", "project-name"]
timeout: "1600s"

ちょっと関係ないけど、argsは配列渡ししないと↓こういうエラーになるので注意。

error Command "nuxt-ts build" not found.

で、上記の公式どおり権限などセッティングしたものの下記のエラーが消えない。

Beginning deployment of service [default]...
#============================================================#
#= Uploading 1 file to Google Cloud Storage                 =#
#============================================================#
File upload done.
ERROR: (gcloud.app.deploy) PERMISSION_DENIED: You do not have permission to act as 'project-name@appspot.gserviceaccount.com'
- '@type': type.googleapis.com/google.rpc.ResourceInfo
  description: You do not have permission to act as this service account.
  resourceName: project-name@appspot.gserviceaccount.com
  resourceType: serviceAccount

f:id:uyamazak:20210222134452p:plain

いろいろ見ていくと、Cloud Buildは{Project ID}@cloudbuild.gserviceaccount.comを使うはずなのに
{Project ID}@appspot.gserviceaccount.comを使っているので気になる。

Cloud Buildの設定を見直したところ「サービス アカウント ユーザー」というそれっぽいロールがあったので有効化したところ、無事成功しました。

f:id:uyamazak:20210222134452p:plain

GCPの教科書

GCPの教科書

Google Apps ScriptからChartworkに投稿する

社内システムで最初、JavaScriptでブラウザからAPI叩こうとしたら、おそらくChatwork API側がPreflight requestに対応してないのが原因でCORSエラーが出て送れない・・・。
そのためGASのウェブアプリ側で送るようにしました。

developer.mozilla.org


UrlFetchApp.fetchで簡単に送れました。

APIについて詳細は公式

developer.chatwork.com

const chatworkRoomId = {ルームIDいれてね}
const chatworkApiToken = '{APIトークンいれてね}'

function sendChatWork (message) {
  const apiUrl = `https://api.chatwork.com/v2/rooms/${chatworkRoomId}/messages`;
  UrlFetchApp.fetch(apiUrl, {
    method: 'post',
    headers: {'X-ChatWorkToken': chatworkApiToken},
    payload: {body: message},
  });
}
// 使い方
sendChatWork('test')

Google Apps Scriptのウェブアプリでaxiosでリクエストする時にハマったメモ

シニアジョブで簡単な来客記録システムを作りあたり、Google Workspaceを使っているので、
NuxtJSでUI作って、Google スプレッドシートとGASでいけるんじゃね?
と思ってやってみたら、ちょっとハマったものの出来たのでメモ。

f:id:uyamazak:20210218134626p:plain
※画面は開発中のものです(圧倒的いらすとや感)

NuxtJSの方は今回は省略。

結論

設定ちゃんとすれば、普通のURLに普通のリクエストでスプレッドシートに書き込むようなAPIが簡単にできる。
認証エラーでもCORSエラーになる罠に気をつける。

ログを書き込むスプレッドシートをつくる

特に何の変哲もないやつ作ります。
f:id:uyamazak:20210218132559p:plain

スクリプトを書く

ツール → スクリプトエディタ でGASの入力画面へ。

今回はGETリクエスト使うのでdoGet()という名前にする必要があります。

詳細は公式。GAS関係は結構デタラメな古い情報が検索に溢れているので注意しましょう。
https://developers.google.com/apps-script/guides/web


名前、会社名とかをGETパラメーターで受け取るようにして雑にこんな感じになりました。
認証とかライブラリインストールとか省けるのでいいですね。
空白行とかできるとアレなので簡単に有無チェックだけはしておきます。

function doGet(e) {
  const ss = SpreadsheetApp.getActive().getSheetByName('来客ログ');
  if (!e) {
    return ContentService.createTextOutput('{result:"NG", error: "eventがありません"}')
  }
  const params = e.parameter
  const name = params.name
  if (!name) {
    return ContentService.createTextOutput('{result:"NG", error: "nameは必須です"}')
  }
  const description = params.description
  if (!description) {
    return ContentService.createTextOutput('{result:"NG", error: "descriptionは必須です"}')
  }
  const company = params.company

  ss.appendRow([new Date(), company, name, description]);
  return ContentService.createTextOutput('{result:"OK"}');
}

デプロイでウェブアプリを選択

デプロイには実行可能 APIウェブアプリの2つがあり、名前的に実行可能 APIの方かな?と思ったけど404になってだめでした。
ウェブアプリの方でした。

アクセスできるユーザーを"全員"にする

この画面で全員にしておかないと認証画面にとばされaxiosでCORSエラーになります。
開発者ツールでリクエストをよく見れば分かるものの、認証エラーではなくCORSエラーで来るのが曲者でした。
f:id:uyamazak:20210218133543p:plain

全員と社内だとURLが変わるので注意しましょう。
また設定変更後デプロイしないと反映されません。

axiosでリクエストする

今回はNuxtJSだったのでプラグインでメソッドを生やしました。
上記で取得したURLはnuxt.config.jsのenvに追加。
あとは必要なパラメーターをつけるだけ。

plugins/gas-api.js

import axios from 'axios'
async function addSpreadSheet(name, company, description) {
  await axios.get(`${process.env.gasApiUrl}`, {
    params: { name, company, description },
  })
}

export default (_, inject) => {
  inject('addSpreadSheet', addSpreadSheet)
}

あとはコンポーネントのmethodsでこんな感じのを追加して使いました。

send() {
      this.$refs.form.validate()
      this.isLoading = true
      if (this.valid) {
        // 送信処理
        try {
          await this.$addSpreadSheet(this.name, this.company, 'テスト')
        } catch(e) {
          this.isLoading = false
          this.$router.push('error')
          return
        }
        this.isLoading = false
        this.$router.push('thanks')
      }
    },