ShinyアプリをGoogle Cloudにデプロイする

[Rでふるさと納税のマップを眺める](https://qiita.com/xjorv/items/d7e25cfc312b0926c5d4)shinyアプリを作成したので、これをGoogle Cloudにデプロイしてみました。

[https://furusato-map-oxbrmwzuuq-dt.a.run.app](https://furusato-map-oxbrmwzuuq-dt.a.run.app)

Google Cloudにデプロイする手順を備忘録として記録しておきます。以下の2つのページを参考にデプロイしています。

[Dockerizing and Deploying a Shiny Dashboard on Google Cloud](https://towardsdatascience.com/dockerizing-and-deploying-a-shiny-dashboard-on-google-cloud-a990ceb3c33a)

[Deploying an R Shiny Dashboard on GCP Cloud Run](https://medium.com/@hdpoorna/deploying-an-r-shiny-dashboard-on-gcp-cloud-run-c1c32a076783)

## app.Rの準備

Shinyアプリは3ファイル（global.R, ui.R, server.R）から1ファイル（app.R）に変更します。最終行に`shinyApp(ui = ui, server = server)`を加えます。

```r:app.R
library(tidyverse)
library(shiny)
library(sf)
library(shinythemes)
library(leaflet)

options(scipen=100) # 桁の大きい数値を指数表示にしない

vec <- 
  c("pop", "pop_M", "pop_F", "pop_2015", "pop_change", "pop_change_rate",
    "pop_density", "household", "Number_donations_accepted", "Donation_amount_accepted",
    "Number_donations_from_outside", "Donation_amount_from_outside", "Costs_procure_gifts",
    "Costs_return_gifts", "Costs_PR", "Costs_settlement", "Costs_Administration",
    "Costs_others", "Total_costs", "Donation_amount_per_pop", "Donation_number_per_pop")

names(vec) <- 
  c("人口（2020年）", "男性人口", "女性人口", "人口（2015年）", "5年間の人口増減数", 
    "5年間の人口増減率", "人口密度", "世帯数", "受け入れた寄付件数", "受け入れた寄附金額", 
    "市町村外から受け入れた寄付件数", "市町村外から受け入れた寄付金額", "返礼品等の調達に係る費用",
    "返礼品等の送付に係る費用", "広報に係る費用", "決済等に係る費用", "事務に係る費用", 
    "その他の費用", "費用合計", "一人当たりの市町村外から受け入れた寄付金額",
    "一人当たりの市町村外から受け入れた寄付件数")

vec_nin <- c("pop", "pop_M", "pop_F", "pop_2015", "pop_change", "pop_density")
vec_ken <- c("Number_donations_accepted", "Number_donations_from_outside", "Donation_number_per_pop")
vec_yen <- c(
  "Donation_amount_accepted", "Donation_amount_from_outside", "Costs_procure_gifts",
  "Costs_return_gifts", "Costs_PR", "Costs_settlement", "Costs_Administration",
  "Costs_others", "Total_costs", "Donation_amount_per_pop")

ui <- fluidPage(
    theme = shinytheme("united"),
    
    titlePanel("ふるさと納税データマップ"),
    
    tabsetPanel(
      tabPanel(
        "マップ",
        div(leafletOutput("leafletPlot", height="100%"), style = "height: 86vh"),
        
        absolutePanel(top = 120, right=20, 
                selectInput(
                  "year", "年度",
                  c("2017", "2018", "2019", "2020", "2021", "2022", "2023"), selected="2023"),
                
                selectInput(
                  "col_id", "表示する項目",
                  c("人口（2020年）", "男性人口", "女性人口", "人口（2015年）", "5年間の人口増減数", 
                    "5年間の人口増減率", "人口密度", "世帯数", "受け入れた寄付件数", "受け入れた寄附金額", 
                    "市町村外から受け入れた寄付件数", "市町村外から受け入れた寄付金額", "返礼品等の調達に係る費用",
                    "返礼品等の送付に係る費用", "広報に係る費用", "決済等に係る費用", "事務に係る費用", 
                    "その他の費用", "費用合計", "一人当たりの市町村外から受け入れた寄付金額",
                    "一人当たりの市町村外から受け入れた寄付件数"), selected="一人当たりの市町村外から受け入れた寄付金額", width="600px")
          ),
        
        tags$footer("一人あたりの値は2020年人口を利用して計算しているため、正確ではありません。")
      ),
        
      tabPanel(
        "参考文献等",
        br(),
        p("出典：調査項目を調べる－国勢調査（総務省）令和２年国勢調査 人口等基本集計　（主な内容：男女・年齢・配偶関係，世帯の構成，住居の状態，母子・父子世帯，国籍など）"),
        tags$a(href="https://www.e-stat.go.jp/stat-search/files?page=1&layout=datalist&toukei=00200521&tstat=000001136464&cycle=0&tclass1=000001136466&tclass2val=0&metadata=1&data=1", "政府統計の総合窓口（e-Stat）該当ページ"),
        br(),
        br(),
        p("出典：国土交通省国土数値情報ダウンロードサイト 行政区域データ(2024年) 全国データ"),
        tags$a(href="https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N03-2024.html", "国土数値情報ダウンロードサイト 該当ページ"),
        br(),
        br(),
        p("出典：総務省 ふるさと納税ポータルサイト 関連資料 受入額の実績等"),
        tags$a(href="https://www.soumu.go.jp/main_sosiki/jichi_zeisei/czaisei/czaisei_seido/furusato/archive/", "ふるさと納税ポータルサイト 該当ページ")
      )
    )
  )

server <- shinyServer(
    function(input, output, session) {

    output$leafletPlot <- renderLeaflet({
    
    geofile <- paste0("furusato", input$year, ".geojson")  
    
    geo <- st_read(geofile)

    geo <- geo |> filter(!st_is_empty(geo)) # geometryにemptyがあるとleafletで取り扱いできない
    
    col_selected <- vec[input$col_id]
    
    val <- geo[col_selected]  |> st_drop_geometry() |> unlist()
    
    if(col_selected %in% vec_nin){
      char_val <- 
        paste0(format(val, big.mark = ",", scientific = F) |> str_trim(), "人")
    } else if(col_selected %in% vec_ken){
      char_val <- 
        paste0(format(val |> round(2), big.mark = ",", scientific = F) |> str_trim(), "件")
    } else if(col_selected %in% vec_yen){
      char_val <- 
        paste0(format(val |> round(2), big.mark = ",", scientific = F) |> str_trim(), "円")
    } else if(col_selected == "pop_change_rate"){
      char_val <- 
        paste0(val |> round(2), "%")
    } else if(col_selected == "household"){
      char_val <- 
        paste0(format(val, big.mark = ",", scientific = F) |> str_trim(), "軒")
    }
    
    bins <- quantile(val |> na.omit(), probs=seq(0, 1, by=0.1)) |> round(5)
    pal <- colorBin("Spectral", domain = val, bins = bins, reverse=TRUE)
    
    geo |> 
      leaflet() |> 
      addProviderTiles(providers$OpenStreetMap) |> 
      setView(137.5, 37.5, zoom = 6) |> 
      addPolygons(
        fillColor = ~pal(val),
        weight = 1,
        opacity = 1,
        color = "white",
        dashArray = "3",
        fillOpacity = 0.5,
        highlightOptions = highlightOptions(
          weight = 5,
          color = "#666",
          dashArray = "",
          fillOpacity = 0.3,
          bringToFront = TRUE),
        label = map2_chr(geo$city, char_val, paste),
        labelOptions = labelOptions(
          style = list("font-weight" = "normal", padding = "3px 8px"),
          textsize = "15px",
          direction = "auto")) |> 
      addLegend(pal = pal, values = ~val, opacity = 0.7, title = names(vec)[input$col_id],
                position = "bottomright")
    })
  }
)

shinyApp(ui = ui, server = server)
```

## Dockerfileの準備

次にrocker::shiny-verseを参照して、Dockerfileを準備します。

```dockerfile:Dockerfile
FROM rocker/shiny-verse:latest

RUN apt-get update && apt-get install -y \
    curl \
    sudo \
    pandoc \
    pandoc-citeproc \
    libcurl4-gnutls-dev \
    libcairo2-dev \
    libxt-dev \
    libssl-dev \
    libssh2-1-dev\
    libssl3 \ # 以下の行がないとsfのインストールがうまくいかない
    libgdal-dev \
    libproj-dev \
    libgeos-dev \
    libudunits2-dev \
    netcdf-bin \
    libharfbuzz-dev \ 
    libfribidi-dev \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/ \
    && rm -rf /tmp/downloaded_packages/ /tmp/*.rds

RUN R -e "install.packages('shiny')"
RUN R -e "install.packages('shinythemes')"
RUN R -e "install.packages('tidyverse')"
RUN R -e "install.packages('sf', dependencies = TRUE)"
RUN R -e "install.packages('leaflet', dependencies = TRUE)"
RUN R -e "install.packages('units', dependencies = TRUE)"

RUN rm -rf /tmp/downloaded_packages/ /tmp/*.rds

COPY shiny-server.conf  /etc/shiny-server/shiny-server.conf

COPY app /srv/shiny-server/

RUN rm /srv/shiny-server/index.html

EXPOSE 3838

COPY shiny-server.sh /usr/bin/shiny-server.sh

RUN ["chmod", "+x", "/usr/bin/shiny-server.sh"]

USER shiny

CMD ["/usr/bin/shiny-server"]
```

上記のDockerfileは参考ページのものほとんどそのままです。libgdal-devなどがsfのインストールに必須で、無いとShiny Serverを立ち上げた時にエラーとなります。

別途上記参考ページに記載されているshiny-server.shとshiny-server.confを準備します。ファイル構造は以下の通りです。

furusato
|- app
|  |- app.R
|  |- furusato2017.geojson
|  |- furusato2018.geojson
|  |- furusato2019.geojson
|  |- furusato2020.geojson
|  |- furusato2021.geojson
|  |- furusato2022.geojson
|  |- furusato2023.geojson
| - Dockerfile
| - shiny-server.conf
| - shiny-server.sh

## Dockerのビルドとローカル環境での確認

Dockerをbuildし、ローカル環境でdocker runしてShinyアプリが動くかどうか確認します。

```shell
docker build -t furusato_map .

# ローカルでDockerを走らせて様子を見る
docker run -e APPLICATION_LOGS_TO_STDOUT=true --user shiny -p 3838:3838 furusato_map
```

この段階で問題なければ、Google Cloudの設定に移行します。

## gcloud CLIのインストールと準備

まずはshellから`gcloud`コマンドラインを使えるようにするため、gcloud CLIをインストールします。インストールは以下のサイトに従い行います。

[gcloud CLI をインストールする](https://cloud.google.com/sdk/docs/install?hl=ja)

次に、Auth loginを行い、Google Cloudのdockerの設定を行います。

```shell
gcloud services enable containerregistry.googleapis.com # Artifact Registryを有効化
gcloud auth login 
gcloud auth configure-docker
```

Google CloudでプロジェクトIDを調べておき、docker pushでファイルをGoogle Cloudに移行します。

```shell
docker tag {REGION}-docker.pkg/dev/{PROJECT_ID}/furusato/furusato_map:v1.0 gcr.io/{PROJECT_ID}/furusato/furusato_map:v1.0
docker push gcr.io/{PROJECT_ID}/furusato/furusato_map:v1.0
```

ここからはGoogle Cloudのページで作業します。プロジェクトのページを開き、Cloud Runを選択します。サービスの作成を選択し、先ほどdocker pushで移行したイメージを選択します。サービス名、リージョンを選択し、コンテナのポートは3838とします。メモリとCPUを最小にするとleafletが動かないため、メモリを2GB、CPUを2とし、作成します。うまくイメージが作成されると、URLが示されるのでそのURLに移動するとShinyアプリがWeb上で確認できるようになります。

![app_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80714/3250c075-a85a-9a24-3be4-417471840044.png)