Google App Script(GAS)で スクレイピングとAPIコール で情報を取得 & スプレッドシートのバックアップ を定期実行する
はじめに
Google App Scriptを確認あたってメモしておいた方がいいと思った部分について記載します。
主な内容は以下の通り。
- サイトをスクレイピングする
- APIコールしてJSONをパースする
- 機密情報をプロパティに設定する
- プロパティから値を参照する / Basic認証
- スプレッドシートをバックアップする
- GASを定期実行する
スクレイピングする
"App Script" の画面で 「ライブラリ」を追加を押して 以下のスクリプトIDを入力して追加する。追加したら "Parser" が使えるようになります。
スクリプトID:1Mc8BthYthXx6CoIz90-JiSzSafVnT6U3t0z_W3hLTAX5ek4w0G_EIrNw
サイトに直接スクレイピングする場合
■ スクレイピングしているコードの一部抜粋
"UrlFetchApp" を利用する
以下のコードは kakaku.comで公開されている投資信託の基準価格を取得するコード
- "isinCode" は 投資信託に割り振られているコードです。
// kakaku.comが公開しているサイトへスクレイピング function fetchInvestmentTrustPriceFromKakaku(isinCode) { let url = "https://kakaku.com/fund/detail.asp?si_isin=" + isinCode; let html = UrlFetchApp.fetch(url).getContentText(); // Parser.data(‘抽出データ').from(‘開始文字列').to(‘終了文字列').build() let basePrice = Parser.data(html).from('<span class="price">').to('</span>').build(); return basePrice }
- 以下のコードは金価格を取得するコードです。
// MyGoldPartnerから金価格をスクレイピング function fetchGoldPrice() { const url = "https://gold.mmc.co.jp/market/gold-price/" let html = UrlFetchApp.fetch(url).getContentText(); let goldPrices = Parser.data(html).from('<span class="c-table__text--xl">').to('</span>').iterate(); return [goldPrices[4], goldPrices[5]]; }
GAEで実装したAPIをコールする場合
GAEで実装しているAPIはJSONで返すので jsonをパースする処理にしています。
/* レスポンス例 { "time": "2022-02-16T21:44:10.569731882Z", "base_price": 10001, "day_before_price": 0 } */ function fetchInvestmentTrustPriceFromGAE(gaeEndpoint, requestPath, isinCode, urlOptions) { let url = gaeEndpoint + requestPath + isinCode let res = UrlFetchApp.fetch(url, urlOptions); let resJson = JSON.parse(res.getContentText()); return resJson; } // 自前のGoogle App Engine (GAE) 金価格を取得 /* レスポンス例 { "time": "2022-02-17T21:04:11.755729+09:00", "gold": 7656, "platinum": 4373 } */ function fetchGoldPriceFromGAE(gaeEndpoint, requestPath, urlOptions) { let url = gaeEndpoint + requestPath let res = UrlFetchApp.fetch(url, urlOptions); let resJson = JSON.parse(res.getContentText()); //let goldPrice = resJson['gold'] return resJson; }
GASに機密情報を登録する
以前は管理画面にて登録できたようですが、今はコードから登録する必要があったので以下のコードから登録しました。 今回登録したのは、GAEをベーシック認証をかけているので "ENDPOINT" と "ID" と "PW" を登録しました。
- プロパティに登録するコード
function setProperty() { let prop = PropertiesService.getScriptProperties(); /* ex let secrets = { "hoge": "abc", "fuga": "123" } */ let secrets = { "ENDPOINT": "GAEのエンドポイントを設定", "ID": "xxx", "pass": "yyy" } prop.setProperties(secrets) }
- 登録したプロパティから値を参照するコード(例)
const gaeEndpoint = PropertiesService.getScriptProperties().getProperty("ENDPOINT") // GAEのエンドポイン
- Basic認証をするために ( urlOptions として認証情報を渡す )
const gaeEndpoint = PropertiesService.getScriptProperties().getProperty("ENDPOINT") // GAEのエンドポイン const basicAuthId = PropertiesService.getScriptProperties().getProperty("ID") // Basic認証のID(セットプロパティから取得) const basicAuthPass = PropertiesService.getScriptProperties().getProperty("PASS") // Basic認証のPW(セットプロパティから取得) // Basic認証 let urlOptions = { method: "GET", headers: { "Authorization": "Basic " + Utilities.base64Encode(basicAuthId + ":" + basicAuthPass) } } function fetchInvestmentTrustPriceFromGAE(gaeEndpoint, requestPath, isinCode, urlOptions) { // 省略
シートを別のスプレッドシートにバックアップするコード
function execBackup() { // 保存元のシートID const originalSheetId = 'オリジナルシートID'; // 保存先のシートID const backupSheetId = '保存先のシートID; // 保存元データのシート名(保存先データのシート名と同一) const sheets = ['sheet_A', 'sheet_B', 'sheet_C'] // 対象のシート名をセット // シート毎に処理 for (let i = 0; i < sheets.length; i++) { // オリジナルデータの設定 let originalSheet = SpreadsheetApp.openById(originalSheetId); let originalSheetName = originalSheet.getSheetByName(sheets[i]); // バックアップ先の設定 let backupSheet = SpreadsheetApp.openById(backupSheetId); let backupSheetName = backupSheet.getSheetByName(sheets[i]); // 保存先のデータを削除 backupSheetName.clear(); let lastRow = originalSheetName.getLastRow(); let lastColumn = originalSheetName.getLastColumn(); let copyData = originalSheetName.getRange(1, 1, lastRow, lastColumn).getValues(); // データの書き込み backupSheetName.getRange(1, 1, lastRow, lastColumn).setValues(copyData); } }
GASのスクリプトを定期実行する
function execBackup() {}
を作成したのでバックアップを実行するときは execBackup
を選択。
- トリガーを選択
- トリガーを追加
- 実行する関数名をプルダウンメニューから選択
Echo ( GoのWebフレームワーク ) on GAE で貴金属 ( 金 & プラチナ )の価格を返すAPIを作成した
はじめに
以前に 貴金属の価格を取得して可視化する内容で記事を作成しました。 今回は Echo( Goフレームワーク ) でデータを取得する部分についてアップデートした部分についてです。 金融資産のポートフォリオを可視化するために必要な情報を取得するためにデータ取得先、取得データ等を変更し、Echoのバージョンをアップデートしました。
データの取得先
価格を取得するために、マイゴールドパートナー を利用しました。
スクレイピング
スクレイピングは対象サイトに迷惑がかからないようにする必要があります。 価格は基本的に日時で更新されるので1日1回定期アクセスするようにしており、APIを叩く際はベーシック認証をかけて 外部から不用意にアクセスされないように制御しています。
Goでスクレイピングするために、 goquery
を利用しました。
APIについて
リクエストについて
${ENDPOINT}/metal
- リクエストパスは
/metal
と設定
- リクエストパスは
レスポンスについて
- jsonで以下の情報を返す
- 金のweb買取価格
- 金のweb前日比価格
- プラチナのweb買取価格
- プラチナのweb前日比価格
- タイムスタンプ
- jsonで以下の情報を返す
■ レスポンス結果(例)
{ "gold": 7697, "gold_day_before_price": 28, "platinum": 4301, "platinum_day_before_price": -71, "time": "2022/02/2-21:14:28" }
ソースコード
参考までにディレクトリ構造は以下の通り
. ├── README.md ├── api │ ├── base.go │ ├── client.go │ ├── crypto_coincheck.go │ ├── crypto_coincheck_test.go │ ├── crypto_gmo.go │ ├── crypto_gmo_test.go │ ├── healthcheck.go │ ├── investment_trust.go │ ├── metal.go │ └── staticcheck.conf ├── app.yaml ├── authentication │ └── basic_auth.go ├── go.mod ├── go.sum ├── main.go └── secret.yaml
■ main.go
package main import ( "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "google.golang.org/appengine" "github.com/yhidetoshi/apiEchoGAE-private/api" "github.com/yhidetoshi/apiEchoGAE-private/authentication" ) var e = createMux() func createMux() *echo.Echo { e := echo.New() return e } func main() { // Echoインスタンス作成 // http.Handle("/", e) e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.Gzip()) e.Use(authentication.BasicAuth()) // Basic Auth cGmo := api.ClientCryptoGMO{} cGmo.NewClientCryptoGMO() cCc := api.ClientCryptoCoincheck{} cCc.NewClientCryptoCoincheck() // ルーティング // // ヘルスチェック e.GET("/api/healthcheck", api.Healthcheck) // 貴金属の価格を取得 e.GET("/api/metal", api.FetchMetal) // ISINコードを引数に基準価格を取得 // curl -sS localhost:1323/investment-trust/JP90C000GKC6 e.GET("/api/investment-trust/:isin", api.FetchInvestTrust) // 仮想通貨の価格を取得 e.GET("/api/crypto/gmo", cGmo.FetchCryptoGMO) e.GET("/api/crypto/coincheck", cCc.FetchCryptoCoincheck) // LocalとGAEの切り替え appengine.Main() // GAEの場合に有効化 //e.Start(":1323") // Localの場合に有効化 }
■ base.go
package handler import ( "log" "strconv" "time" ) const ( timezone = "Asia/Tokyo" offset = 9 * 60 * 60 timeFormat = "2006/01/02-15:04:05" ) var ( jst = time.FixedZone(timezone, offset) nowTime = time.Now().In(jst).Format(timeFormat) ) func convertStringToInt(value string) int { intValue, err := strconv.Atoi(value) if err != nil { log.Println("Error", err) } return intValue }
■ metal.go
package api import ( "encoding/json" "log" "net/http" "os" "strings" "github.com/PuerkitoBio/goquery" "github.com/labstack/echo/v4" ) /* Response { "gold": 7697, "gold_day_before_price": 28, "platinum": 4301, "platinum_day_before_price": -71, "time": "2022/02/21-21:14:28" } */ const ( targetURLMetal = "https://gold.mmc.co.jp/" goldPriceCount = 8 goldDayBeforePriceCount = 9 platinumPriceCount = 48 platinumDayBeforePriceCount = 49 ) type Metal struct { GoldPrice int `json:"gold"` GoldDayBeforePrice int `json:"gold_day_before_price"` PlatinumPrice int `json:"platinum"` PlatinumDayBeforePrice int `json:"platinum_day_before_price"` Date string `json:"time"` } func FetchMetal(c echo.Context) error { var goldPrice, goldDayBeforePrice, platinumPrice, platinumDayBeforePrice string var tdCount int res, err := http.Get(targetURLMetal) if err != nil { log.Println(err) os.Exit(1) } defer res.Body.Close() doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { log.Println(err) } // Fetch gold and platinum price doc.Find("div.p-table-scroll--sticky > table > tbody > tr > td").Each(func(_ int, s *goquery.Selection) { if tdCount == goldPriceCount { //Gold Price goldPrice = s.Find("span.c-table__text--xl").Text() } if tdCount == goldDayBeforePriceCount { //Gold Price Day Before Price Count goldDayBeforePrice = s.Find("span.c-table__text--xl").Text() } if tdCount == platinumPriceCount { // Platinum Price platinumPrice = s.Find("span.c-table__text--xl").Text() } if tdCount == platinumDayBeforePriceCount { // Platinum Day Before Price Count platinumDayBeforePrice = s.Find("span.c-table__text--xl").Text() } tdCount++ }) // Format strGoldPrice := strings.Replace(goldPrice, ",", "", -1) strGoldDayBeforePrice := strings.Replace(goldDayBeforePrice, "+", "", -1) strPlatinumPrice := strings.Replace(platinumPrice, ",", "", -1) strPlatinumDayBeforePrice := strings.Replace(platinumDayBeforePrice, "+", "", -1) // Convert string to int intGoldPrice := convertStringToInt(strGoldPrice) intGoldDayBeforePrice := convertStringToInt(strGoldDayBeforePrice) intPlatinumPrice := convertStringToInt(strPlatinumPrice) intPlatinumDayBeforePrice := convertStringToInt(strPlatinumDayBeforePrice) // Set value to json responseJSON := Metal{ GoldPrice: intGoldPrice, GoldDayBeforePrice: intGoldDayBeforePrice, PlatinumPrice: intPlatinumPrice, PlatinumDayBeforePrice: intPlatinumDayBeforePrice, Date: nowTime, } response, err := json.Marshal(responseJSON) if err != nil { log.Println(err) } return c.String(http.StatusOK, string(response)) }
■ 確認
❯ curl -sSu ${ID}:${PW} ${ENDPOINT}/metal | jq { "gold": 7698, "gold_day_before_price": 1, "platinum": 4338, "platinum_day_before_price": 37, "time": "2022/02/22-21:19:14" }
デプロイについて
今回のGoアプリはGoogle App Engineにデプロイしているので、Github Actionsで自動デプロイするようにしています。 そちらについては以前に記事でまとめています。
Echo ( GoのWebフレームワーク ) on GAE で投資信託の基準価格を返すAPIを作成した
はじめに
今回は金融資産のポートフォリオ可視化するために投資信託の情報を返してくれるAPIが欲しかったので 勉強も兼ねてEchoで作成してみました。
データの取得先とISINコード
価格を取得するために、価格コムのサイト を利用しました。 kakaku.com
- S&P500の場合のURl
投資信託には ISINコードが割り当てられています。
このサイトでは クエリパラメータに ISINコード
を渡せばその投資信託の基準価格のサイトを表示できます。
スクレイピング
スクレイピングは対象サイトに迷惑がかからないようにする必要があります。 価格は基本的に日時で更新されるので1日1回定期アクセスするようにしており、APIを叩く際はベーシック認証をかけて 外部から不用意にアクセスされないように制御しています。
Goでスクレイピングするために、 goquery
を利用しました。
APIについて
- リクエストについて
${ENDPOINT}/investment-trust/${ISIN_CODE}
- リクエストパスは
/nvestment-trust
と設定 - パスパラメータとして
ISINコード
を指定
- リクエストパスは
- パスパラメータの設定方法については以下を参考
e.GET("/investment-trust/:isin", api.FetchInvestTrust)
- レスポンスについて
- jsonで以下の情報を返す
- 基準価格
- 前日比価格
- タイムスタンプ
- jsonで以下の情報を返す
■ レスポンス結果(例)
{ "base_price": 17569, "day_before_price": -461, "time": "2022/02/21-22:25:42" }
- スクレイピングする対象のhtml(一部抜粋)
<div class="infoMainTop clearfix"> <dl> <dt>基準価額:</dt> <dd><span class="price"> 17,462 </span>円 前日比 <span class="minus">-107</span>円(-0.61%)</dd> </dl>
ソースコード
参考までにディレクトリ構造は以下の通り
. ├── README.md ├── api │ ├── base.go │ ├── client.go │ ├── crypto_coincheck.go │ ├── crypto_coincheck_test.go │ ├── crypto_gmo.go │ ├── crypto_gmo_test.go │ ├── healthcheck.go │ ├── investment_trust.go │ ├── metal.go │ └── staticcheck.conf ├── app.yaml ├── authentication │ └── basic_auth.go ├── go.mod ├── go.sum ├── main.go └── secret.yaml
- Goのバージョン
❯ go version go version go1.17.6 darwin/amd64
■ main.go
package main import ( "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "google.golang.org/appengine" "github.com/yhidetoshi/apiEchoGAE-private/api" "github.com/yhidetoshi/apiEchoGAE-private/authentication" ) var e = createMux() func createMux() *echo.Echo { e := echo.New() return e } func main() { // Echoインスタンス作成 // http.Handle("/", e) e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.Gzip()) e.Use(authentication.BasicAuth()) // Basic Auth cGmo := api.ClientCryptoGMO{} cGmo.NewClientCryptoGMO() cCc := api.ClientCryptoCoincheck{} cCc.NewClientCryptoCoincheck() // ルーティング // // ヘルスチェック e.GET("/api/healthcheck", api.Healthcheck) // 貴金属の価格を取得 e.GET("/api/metal", api.FetchMetal) // ISINコードを引数に基準価格を取得 // curl -sS localhost:1323/investment-trust/JP90C000GKC6 e.GET("/api/investment-trust/:isin", api.FetchInvestTrust) // 仮想通貨の価格を取得 e.GET("/api/crypto/gmo", cGmo.FetchCryptoGMO) e.GET("/api/crypto/coincheck", cCc.FetchCryptoCoincheck) // LocalとGAEの切り替え appengine.Main() // GAEの場合に有効化 //e.Start(":1323") // Localの場合に有効化 }
■ base.go
共通する設定や関数を集約
package handler import ( "log" "strconv" "time" ) const ( timezone = "Asia/Tokyo" offset = 9 * 60 * 60 timeFormat = "2006/01/02-15:04:05" ) var ( jst = time.FixedZone(timezone, offset) nowTime = time.Now().In(jst).Format(timeFormat) ) func convertStringToInt(value string) int { intValue, err := strconv.Atoi(value) if err != nil { log.Println("Error", err) } return intValue }
■ investment_trust.go
package api import ( "encoding/json" "fmt" "log" "net/http" "strings" "github.com/PuerkitoBio/goquery" "github.com/labstack/echo/v4" ) /* Response { "base_price": 17569, "day_before_price": -461, "time": "2022/02/21-15:25:42" } */ var ( targetURL = "https://kakaku.com/fund/detail.asp?si_isin=" ) type InvestmentTrustInfo struct { BasePrice int `json:"base_price"` DayBeforePrice int `json:"day_before_price"` Date string `json:"time"` } func FetchInvestTrust(c echo.Context) error { var strBasePrice, strDayBeforePrice string isin := c.Param("isin") res, err := http.Get(targetURL + isin) if err != nil { log.Println(err) } defer res.Body.Close() doc, err := goquery.NewDocumentFromReader(res.Body) if err != nil { fmt.Println("error res.Body reader") log.Println(err) } doc.Find("div.infoMainTop.clearfix").Each(func(_ int, s *goquery.Selection) { strBasePrice = s.Children().Find("span.price").First().Text() plusPrice := s.Children().Find("span.plus").First().Text() // 前日比がプラスの場合 minusPrice := s.Children().Find("span.minus").First().Text() // 前日比の価格 // htmlタグが結果で変化するため if minusPrice != "" { strDayBeforePrice = minusPrice } else if plusPrice != "" { strDayBeforePrice = plusPrice } if minusPrice != "" && plusPrice != "" { strDayBeforePrice = "0" } }) // trim "," repCommaBasePrice := strings.Replace(strBasePrice, ",", "", -1) repCommaDayBeforePrice := strings.Replace(strDayBeforePrice, ",", "", -1) // trim "\n" repNewLineBasePrice := strings.Replace(repCommaBasePrice, "\n", "", -1) repNewLineDayBeforePrice := strings.Replace(repCommaDayBeforePrice, "\n", "", -1) // trim "*" repPlusDayBeforePrice := strings.Replace(repNewLineDayBeforePrice, "+", "", -1) // Convert string to int intBasePrice := convertStringToInt(repNewLineBasePrice) intDayBeforePrice := convertStringToInt(repPlusDayBeforePrice) // Set value to json responseJSON := InvestmentTrustInfo{ BasePrice: intBasePrice, DayBeforePrice: intDayBeforePrice, Date: nowTime, } response, err := json.Marshal(responseJSON) if err != nil { log.Println(err) } return c.String(http.StatusOK, string(response)) }
■ 確認
❯ curl -sSu ${ID}:${PW} ${ENDPOINT}/investment-trust/JP90C0001MP2 | jq . { "base_price": 10001, "day_before_price": 0, "time": "2022/02/22-21:27:41" }
デプロイについて
今回のGoアプリはGoogle App Engineにデプロイしているので、Github Actionsで自動デプロイするようにしています。 そちらについては以前に記事でまとめています。
claspを利用してGitHub Actionsで Google App Script ( GAS )をデプロイする
はじめに
前回の記事で、google app script (GAS) の開発環境を整えました。なので今回はGASを自動デプロイする環境をGithub Actionsで用意します。
Github Actions
前回の記事にも記載していますが設定するディレクトリ構成は以下の通り
- ディレクトリ構成(必要な部分を抜粋)
❯ tree -L 2 -a . ├── .clasp.json ├── .claspignore ├── .github │ └── workflows └── src ├── crypto.js ├── appsscript.json
- claspでデプロイするために必要な3つのファイル
~/.clasprc.json
.clasp.json
appsscript.json
github actionsの処理で 実行環境に3つのファイルを作成してから $clasp push
コマンドを実行します。
これらのファイルには機密情報が含まれるので githubの secretにそれぞれ登録します。 環境変数名は以下にしました。
- clasprc.json
ACCESS_TOKEN
ID_TOKEN
REFRESH_TOKEN
CLIENTID
CLIENTSECRET
- clasp.json
SCRIPTID
(機密扱いしなくてもいいかもですが念のために)
- appsscript.json
LIBRARYID
(機密扱いしなくてもいいかもですが念のために)
{
→\{
"
→\"
[
→\[
github-actionsのソースコード deploy.yaml
は以下。
appsscript.json
の配置場所に関しては、.clasp.json
で"rootDir": "./src"
と設定しているため。- github actionsの実行結果を見ても、 echoしていますが、secretの部分は *** でマスクされています。
echo \{\"scriptId\":\"***\",\"rootDir\":\"./src\"\} > ./clasp.json
name: Deply to Google App Script on: push: branches: - main env: NODE_VERSION: '16' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: ${{ env.NODE_VERSION }} - name: Install clasp run: | npm init -y npm install clasp -g - name: Setup clasprc.json run: echo \{\"token\":\{\"access_token\":\"${{ secrets.ACCESS_TOKEN }}\",\"scope\":\"https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/script.projects https://www.googleapis.com/auth/script.webapp.deploy https://www.googleapis.com/auth/logging.read openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/script.deployments https://www.googleapis.com/auth/service.management https://www.googleapis.com/auth/cloud-platform\",\"token_type\":\"Bearer\",\"id_token\":\"${{ secrets.ID_TOKEN }}\",\"expiry_date\":1620870307822,\"refresh_token\":\"${{ secrets.REFRESH_TOKEN }}\"\},\"oauth2ClientSettings\":\{\"clientId\":\"${{ secrets.CLIENTID }}\",\"clientSecret\":\"${{ secrets.CLIENTSECRET }}\",\"redirectUri\":\"http://localhost\"\},\"isLocalCreds\":false\} > ~/.clasprc.json - name: Setup clasp.json run: echo \{\"scriptId\":\"${{ secrets.SCRIPTID }}\",\"rootDir\":\"./src\"\} > ./clasp.json - name: Setup appsscript.json run: echo \{\"timeZone\":\"Asia/Tokyo\",\"exceptionLogging\":\"STACKDRIVER\",\"runtimeVersion\":\"V8\",\"dependencies\":\{\"libraries\":\[\{\"userSymbol\":\"Parser\",\"version\":\"8\",\"libraryId\":\"${{ secrets.LIBRARYID }}\"\}\]\}\} > ./src/appsscript.json - name: Deploy run: clasp push
clasp で Google App Script ( GAS ) の開発環境を整えた
はじめに
Google App Script(GAS) でコーディングするにあたりローカルの開発環境をセッティングしたので備忘録です。 GASを実行するには、https://script.google.com/home でプロジェクトを作成してブラウザのエディタでコーディンングができます。 しかし、ブラウザ上でコーディンングするより使い慣れたエディタ(Visual Stdio Code)でやりたかったので clasp というgoogle製のCLIツールを 利用して環境を整えました。
設定
インストール
claspのgithuリポジトリ
node.jsが必要なのでインストールします。ここではインストールされている状態で以下をインストール。
npm init -y npm install @google/clasp -g npm install @types/google-apps-script
Googleアカウントに認証する。
clasp login --no-localhost
これを実行するとブラウザに画面が表示されていくので説明に応じて設定します。
設定で "Google Apps Script API" を有効化します。
[
claspのセッティング
clasp でプロジェクトを作成
$ clasp create Create which script? (Use arrow keys) ❯ standalone
- ディレクトリ構成(一部抜粋)
├── .clasp.json ├── node_modules ├── package-lock.json ├── package.json └── src ├── appsscript.json ├── crypto.js
.clasp.json
scriptId
: 以下のプロジェクトの設定の値rootDir
: jsのソースコードを配置するパスを指定する
{ "scriptId": "XXXXXXXXXXXXXXX", "rootDir": "./src" }
appsscript.json
は rootDirで指定したパスに配置する
claspのコマンド例
$ clasp open # webブラウザのエディタを開く $ clasp push # コードをwebブラウザのエディタにアップロード $ clasp pull # webブラウザのエディタのコードから取得する
参考
Goアプリを Google App Engine ( GAE ) に GitHub Actionsでデプロイする(Workload Identity 連携)
はじめに
前回の記事でGitHub ActionsでGoogle App Engine (GAE) にデプロイする時の認証にサービスアカウントの秘密鍵を利用しました。 そこで今回はOpenID Connectを利用して実行します。秘密鍵を管理する必要もなく、githubのsecretsに登録しないのでより安全に利用できると思います。
Google Cloudの設定( IAM と Workload Identity )
Google Cloud側で必要となる設定を以下で作成する。(任意に設定する名前を環境変数にセットして上から順番に実行していく)
# 設定変数を任意に設定 export PROJECT_ID="project-id" export SERVICE_ACCOUNT_NAME="sa-account-name" export WORKLOAD_IDENTITY_POOLS="wi-pools" export WORKLOAD_IDENTITY_POOLS_DISPLAY="wi-pools-display" export WORKLOAD_IDENTITY_PROVIDER_DISPLAY="wi-provider-display" export OIDC_NAME="oidc-name" export GITHUB_REPO="account/repo-name" # サービスアカウントを作成 gcloud iam service-accounts create "${SERVICE_ACCOUNT_NAME}" \ --project "${PROJECT_ID}" \ --display-name "${SERVICE_ACCOUNT_NAME}" # サービスアカウントに権限を付与 gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --role="roles/iam.serviceAccountUser" \ --member="serviceAccount:${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" # IAM Service Account Credentials APIを有効にする gcloud services enable iamcredentials.googleapis.com \ --project "${PROJECT_ID}" # Workload Identity プールを作成 gcloud iam workload-identity-pools create "${WORKLOAD_IDENTITY_POOLS}" \ --project="${PROJECT_ID}" \ --location="global" \ --display-name="${WORKLOAD_IDENTITY_POOLS_DISPLAY}" # Workload IdentityプールのIDを取得 gcloud iam workload-identity-pools describe "${WORKLOAD_IDENTITY_POOLS}" \ --project="${PROJECT_ID}" \ --location="global" \ --format="value(name)" # Workload IdentityプールのIDを環境変数に設定 export WORKLOAD_IDENTITY_POOL_ID="上のレスポンス結果" # Workload IdentityプールにWorkload Identityプロバイダを作成 gcloud iam workload-identity-pools providers create-oidc "${OIDC_NAME}" \ --project="${PROJECT_ID}" \ --location="global" \ --workload-identity-pool="${WORKLOAD_IDENTITY_POOLS}" \ --display-name="${WORKLOAD_IDENTITY_PROVIDER_DISPLAY}" \ --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \ --issuer-uri="https://token.actions.githubusercontent.com" # 指定したGithubリポジトリで利用できるようにサービスアカウントに設定を追加 gcloud iam service-accounts add-iam-policy-binding "${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \ --project="${PROJECT_ID}" \ --role="roles/iam.workloadIdentityUser" \ --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${GITHUB_REPO}" ## Github Actionsで指定する値を確認 ## # この結果を Github Actions (google-github-actions/auth)の 'workload_identity_provider' に設定する gcloud iam workload-identity-pools providers describe "${OIDC_NAME}" \ --project="${PROJECT_ID}" \ --location="global" \ --workload-identity-pool="${WORKLOAD_IDENTITY_POOLS}" \ --format='value(name)' # この結果を Github Actions (google-github-actions/auth)の 'service_account' に設定する gcloud iam service-accounts describe "${SERVICE_ACCOUNT_NAME}"@"${PROJECT_ID}".iam.gserviceaccount.com --format="json" | jq .email -r もしくは gcloud iam service-accounts describe "${SERVICE_ACCOUNT_NAME}"@"${PROJECT_ID}".iam.gserviceaccount.com --format="yaml" | yq .email
作成したサービスアカウントに対して、GAEにデプロイするための権限を付与します。(google cloudへの認証だけであればこの権限追加は不要)
権限
- Storage オブジェクト閲覧者
- Storage オブジェクト作成者
- Cloud Build サービス アカウント
- App Engine デプロイ担当者
参考
- IAM周りとworkload-identityの作成について参考にさせていただきました。
GitHub Actionsについて
WORKLOAD_IDENTITY_PROVIDER
と GCP_SERVICE_ACCOUNT
を githubのsecretsに登録する。
値については、上記の( "Google Cloudの設定( IAM と Workload Identity" の "Github Actionsで指定する値を確認") で確認 )
主な処理は以下の通り。
■ 利用するGIthub-Actionsのライブラリ
- Google Cloudの認証
- Google App Engineにデプロイ
name: Deploy Google App Engine on: push: branches: - main paths: - ./** - .github/workflows/deploy.yaml permissions: id-token: write contents: read env: BASIC_AUTH_ID: ${{ secrets.BASIC_AUTH_ID }} BASIC_AUTH_PW: ${{ secrets.BASIC_AUTH_PW }} BASIC_AUTH_PATH: /metal WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} GCP_SERVICE_ACCOUNT: ${{ secrets.GCP_SERVICE_ACCOUNT }} jobs: deploy: runs-on: ubuntu-latest steps: - uses: 'actions/checkout@v2' - name: 'Authenticate to Google Cloud by OIDC' id: 'gcp-auth-oidc' uses: 'google-github-actions/auth@v0.6.0' with: workload_identity_provider: '${{ env.WORKLOAD_IDENTITY_PROVIDER }}' service_account: '${{ env.GCP_SERVICE_ACCOUNT }}' - name: 'Deploy to App Engine' id: 'deploy' uses: 'google-github-actions/deploy-appengine@v0.6.0' with: deliverables: 'app.yaml' promote: false version: 'v1' - name: 'validate' run: curl -sS -u ${{ env.BASIC_AUTH_ID }}:${{ env.BASIC_AUTH_PW }} ${{ steps.deploy.outputs.url }}${{ env.BASIC_AUTH_PATH }}
■ github actionsの実行結果
参考
Workload Identity 連携 について
Goアプリを Google App Engine ( GAE ) に GitHub Actionsでデプロイする(秘密鍵を利用する場合)
はじめに
今回はGithub ActionsでGoogle App Engine ( GAE ) に デプロイします。認証にはサービスアカウントの秘密鍵を利用します。 後にOpenID Connect(OIDC)を利用するケースについてもまとめます。
以下の記事でGoのフレームワーク(Echo)で作成した貴金属の価格をjsonで返すAPIをGoogle App Engine ( GAE ) にデプロイしました。 そこで今回はこのアプリをmainブランチにマージしたらGAEに自動デプロイできるようにします。
利用するGIthub-Actionsのライブラリ
- Google Cloudの認証
- Google App Engineにデプロイ
サービスアカウントを作成
作成したサービスアカウントに以下の権限を付与して秘密鍵を発行する。
- 権限
- Storage オブジェクト閲覧者
- Storage オブジェクト作成者
- Cloud Build サービス アカウント
- サービス アカウント ユーザー
- App Engine デプロイ担当者
GitHub Actionsについて
githubのsecretsに GCP_SA_KEY
を作成して、発行したjson形式を貼り付けて保存する。
コードは以下の通り。
- Google Cloudに認証
- App Engineをデプロイ
- デプロイしたエンドポイントに
curl
で動作確認
name: Deploy Google App Engine on: push: branches: - main paths: - ./** - .github/workflows/deploy.yaml permissions: id-token: write contents: read env: BASIC_AUTH_ID: ${{ secrets.BASIC_AUTH_ID }} BASIC_AUTH_PW: ${{ secrets.BASIC_AUTH_PW }} BASIC_AUTH_PATH: /metal GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} jobs: deploy: runs-on: ubuntu-latest steps: - uses: 'actions/checkout@v2' - name: 'Authenticate to Google Cloud' uses: 'google-github-actions/auth@v0.6.0' with: credentials_json: '${{env.GCP_SA_KEY }}' - name: 'Deploy to App Engine' id: 'deploy' uses: 'google-github-actions/deploy-appengine@v0.6.0' with: deliverables: 'app.yaml' promote: false version: 'v1' - name: 'validate' run: curl -sS -u ${{ env.BASIC_AUTH_ID }}:${{ env.BASIC_AUTH_PW }} ${{ steps.deploy.outputs.url }}${{ env.BASIC_AUTH_PATH }}
■ github actionsの実行結果