GCP上の分析環境構築～Rstudio&Jupyterlab

# 一、目的・背景
データサイエンティストに対して、重い学習ができ、モデル共有や再利用できるようクラウド上の分析環境を用意します。
データ基盤がGCP上で構築されデータウェアハウスがBigqueryに置いていて、社内の不特定多数人利用（部署内のデータサイエンティスト及びインターン生）するため、スケーラブル且つセキュリティ的なニーズに応じて、GCP上のGKEサービスを選定しました。
VertexAIを利用しない理由としては、①Rstudio向けてGPUが提供していない；②環境内部の細かく制御ができない；③コスパの考慮、

# 二、事前知識
１、GCPの基礎知識（APIを有効にする、権限付与、コマンド実行など）
２、ネットワークの色々（IP、domain、portなど）
３、kubectlの知識（Docker、yamlファイルなど）


# 三、アーキテクチャ＆実装
phase１とphase２を分けて、phase１は基本利用できるサービスを提供することで、phase２はwebappデプロイし、ユーザが好きな時間帯でIDEの立ち上げを自動化にします。
![構成図２.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646301/881923a0-7e11-8331-4c2b-bb13d7371ed7.png)

## phase１

### １、domainについて
GCPのIAPを利用するではドメイン名が必要ですので、今回はGCPのCloud Domainsからドメイン名を買いました。
→https://cloud.google.com/blog/ja/products/networking/introducing-cloud-domains
CloudDNSのゾーンを作成してレジストラにレコードを登録します。

### ２、VPC作成＆設定
※cloudShell上で実行＆適当な権限を持ち＆APIを有効にする
安全性を考慮した上、GKE限定公開クラスター作成しようと決まりました。
専用のVPCネットワークまず作成します。
→https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters?hl=ja
①vpc＆サブネットの作成
VPC
```
gcloud compute networks create gke-vpc \
    --subnet-mode=custom \
    --bgp-routing-mode=regional \
    --mtu=1460
```
Subnet追加
![net3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646301/53c209b6-62eb-8018-9c64-d1cc769e1e88.png)
※限定公開のGoogleアクセスを「オン」にする
![net4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646301/a840f518-7b85-6092-c994-8df74e948378.png)
![net1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646301/f037d877-89ec-527a-ac2c-98b675d4bfe4.png)

②ファイアウォールの作成：
deny-all-up、deny-all-down
![net2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646301/3972b73e-e27a-1c7e-85a2-61ab62baf803.png)


### ３、静的IPアドレスの予約
GKEサービス用の外部静的IPアドレスを予約します。
外部静的IPアドレスの予約
```
gcloud compute addresses create <demo-static-ip> --global
```

### ４、GKEクラスターの作成
①クラスター作成
GPUも付け
```
gcloud container clusters create <your-private-cluster-name> \
    --machine-type=<your-machine-type> \
    --num-nodes=<例：3> \
    --disk-size=100 \
    --region=asia-northeast1 \
    --scopes=cloud-platform\
    --accelerator type=<TYPE>,count=<AMOUNT>\
    --subnetwork=<your-subnet-name>\
    --enable-master-authorized-networks \
    --enable-ip-alias \
    --enable-private-nodes \
    --enable-private-endpoint \
    --master-ipv4-cidr <例：172.16.0.32/28>
```
②Workload Identity pool作成
BigqueryやGCSなどのサービスを利用するために、workload identityの設定が必要です。
https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity?hl=ja
上ではノードプールを作成しましたので、ここではupdateだけ
```
gcloud container node-pools update <your-pool-name> --cluster=<your-private-cluster-name> --zone=asia-northeast1-c --workload-metadata=GKE_METADATA
```
③CloudRouter&CloudNATの作成
ほぼデフォルトの設定なので、割愛させていただきます。

### ５、踏み台VMの作成＆IAPの設定＆ファイアウォールルールの追加
GKEクラスターを弄るためには、踏み台サーバを接続してからします。
そのためここでは踏み台サーバの作成及び設定を行います。
①IAPのAPIを有効にする
②踏み台サーバVM「gke-api-server」作成
※scopes=https://www.googleapis.com/auth/cloud-platform、内部静的IPアドレスを使用、同じVPCネットワーク下に置いてあること
③IAP起動＆ファイアウォールの追加
![fireWALL.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2646301/e7cf3097-d1d9-664b-aa34-de8f179ec789.png)
④VM起動＆設定：[https://cloud.google.com/kubernetes-engine/docs/tutorials/private-cluster-bastion](https://cloud.google.com/kubernetes-engine/docs/tutorials/private-cluster-bastion)
⑤IAMでVMのサービスアカウントにGKEへのアクセス権限を追加
⑥VMのipアドレスをGKEへのアクセス許可
```
gcloud container clusters update <your-private-cluster-name>\
 --enable-master-authorized-networks\
 --master-authorized-networks <vm内部ip>/32\
 --zone asia-northeast1-c
```

### ６、Workload Identity上の権限設定
①踏み台サーバからGKEクラスターに接続、kubectlコマンドを実行
```
gcloud container clusters get-credentials <your-private-cluster-name> --zone=asia-northeast1-c
```
②namespace作成
```
kubectl create namespace <your-namespace>
```
③Kubernetes Service Account作成
```
kubectl create serviceaccount <your-KubernetesServiceAccount> --namespace <your-namespace>
```
④google Service Accountの作成
```
gcloud iam service-accounts create <your-GoogleServiceAccount-name> --project=<your-project-id>
```
⑤google Service Accountの権限
```
gcloud projects add-iam-policy-binding dev-analysis-mountain\
 --member "serviceAccount:<your-GoogleServiceAccount>" --role "editor"
```
⑥GSAとKSAの権限（in cloud shell）
```
gcloud iam service-accounts add-iam-policy-binding <your-GoogleServiceAccount-name>\
 --role roles/iam.workloadIdentityUser\
 --member "serviceAccount:<your-project-id>.svc.id.goog[<your-namespace>/<your-KubernetesServiceAccount>]"
```
⑦GSAとKSAの権限（in VM）
```
kubectl annotate serviceaccount <your-KubernetesServiceAccount>\
 --namespace <your-namespace> iam.gke.io/gcp-service-account=<your-GoogleServiceAccount-name>
```

### ７、dockerファイル

#### ①Rstudio
Dockerfile（from RockerProject: https://rocker-project.org/images/versioned/rstudio.html）
```
FROM rocker/r-ver:4.2.2

LABEL org.opencontainers.image.licenses="GPL-2.0-or-later" \
      org.opencontainers.image.source="https://github.com/rocker-org/rocker-versioned2" \
      org.opencontainers.image.vendor="Rocker Project" \
      org.opencontainers.image.authors="Carl Boettiger <cboettig@ropensci.org>"

ENV S6_VERSION=v2.1.0.2
ENV RSTUDIO_VERSION=2022.12.0+353
ENV DEFAULT_USER=rstudio
ENV PANDOC_VERSION=default
ENV QUARTO_VERSION=default

RUN /rocker_scripts/install_rstudio.sh
RUN /rocker_scripts/install_pandoc.sh
RUN /rocker_scripts/install_quarto.sh

RUN apt-get update -y && apt-get install -y vim
RUN apt-get install -y nginx
COPY ./nginx.conf /etc/nginx/
COPY ./init.sh /etc/
RUN /etc/init.d/nginx restart

# gcsfuse mount
RUN apt-get update -y && apt-get install -y gnupg2
# RUN echo "deb http://packages.cloud.google.com/apt gcsfuse-jessie main" | tee /etc/apt/sources.list.d/gcsfuse.list
# RUN wget https://packages.cloud.google.com/apt/doc/apt-key.gpg
# RUN sudo apt-key add apt-key.gpg
# RUN apt-get update -y && apt-get install -y gcsfuse

# packages install
# your packages

EXPOSE 8787
CMD ["bin/bash","/etc/init.sh"]
```
nginx.conf（root-pathをdomain.com/rstudio/の下に）
```
events {
        worker_connections 768;
}

http {

  map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
  }

        server {
                listen <port>;

                location /rstudio/ {
                rewrite ^/rstudio/(.*)$ /$1 break;
                proxy_pass http://localhost:8787;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection $connection_upgrade;
                proxy_read_timeout 20d;

                # Use preferably
                proxy_set_header X-RStudio-Request https://$host/$request_uri;
                proxy_set_header X-RStudio-Root-Path /rstudio;

        }
        #GCPのhealth checkを通るため
        location ~ /health {
                return 200;
        }
    }
}
```
init.sh
```
/etc/init.d/nginx restart
/init
```

#### ②jupyterlab
Dockerfile
```
#GPUを利用するため下記のgoogle-docker-imageを使用
FROM gcr.io/deeplearning-platform-release/pytorch-gpu

# workdir set
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . ./

# gcsfuse mount
RUN apt-get update -y && apt-get install -y gnupg2
RUN echo "deb http://packages.cloud.google.com/apt gcsfuse-jessie main" | tee /etc/apt/sources.list.d/gcsfuse.list
RUN wget https://packages.cloud.google.com/apt/doc/apt-key.gpg
RUN sudo apt-key add apt-key.gpg
RUN apt-get update -y && apt-get install -y gcsfuse
RUN chmod 555 /app/gcsmount.sh

# JupyterLabをインストール
RUN apt-get install -y vim
RUN python -m pip install --upgrade pip 
RUN pip install jupyterlab 

# jupyterlab起動＆baseurl設定＆token設定
CMD jupyter-lab --allow-root --ip=0.0.0.0 --port=<your-port> --no-browser --LabApp.base_url=/jupyter --LabApp.token=<your-token>
```

#### ③gcsmount.shの内容
GCSバケットの作成や権限設定を割愛させていただきます。
```
mkdir ./<your-folder>
gcsfuse -file-mode=777 -dir-mode=777 <your-bucket> ./<your-folder>
```

#### ④Dockerfileのサミット
docker-image用のレジストリArtifact Registryを作成
```
gcloud artifacts repositories create <your-repo>\
 --project=<your-project-id>\
 --repository-format=docker\
 --location=asia-northeast1\
 --description="Docker repository"
```
CloudBuildでsubmit
```
gcloud builds submit --tag asia-northeast1-docker.pkg.dev/<your-project-id>/<your-repo>/<docker-image-name> .
```

### ８、yamlファイル
踏み台サーバから下記のyamlファイルをデプロイ
#### ①deployment.yaml
Rstudio
parameterはこちらのサイドを参考：https://rocker-project.org/images/versioned/rstudio.html
```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rstudio-deployment
  namespace: <your-namespace>
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rstudio
  template:
    metadata:
      labels:
        app: rstudio
    spec:
      serviceAccountName: <your-KubernetesServiceAccount>
      nodeSelector:
        iam.gke.io/gke-metadata-server-enabled: "true"
        cloud.google.com/gke-nodepool: <your-pool-name>
        cloud.google.com/gke-accelerator: nvidia-tesla-t4
      containers:
      - name: rstudio-app
        image: <your-docker-image>
        ports:
        - containerPort: <port番号>
        env:
          - name: PORT
            value: <port番号>
        securityContext:
            privileged: true
```
jupyterlab
```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jupyter-deployment
  namespace: <your-namespace>
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jupyter
  template:
    metadata:
      labels:
        app: jupyter
    spec:
      serviceAccountName: <your-KubernetesServiceAccount>
      nodeSelector:
        iam.gke.io/gke-metadata-server-enabled: "true"
        cloud.google.com/gke-nodepool: <your-pool-name>
        cloud.google.com/gke-accelerator: nvidia-tesla-t4
      containers:
      - name: jupyter-app
        image: <your-docker-image>
        ports:
        - containerPort: <port番号>
        securityContext:
            privileged: true
```

#### ②ManagedCertificate.yaml
```
apiVersion: networking.gke.io/v1 
kind: ManagedCertificate
metadata:
  name: <your-Certificate>
  namespace: <your-namespace>
spec:
  domains:
    - <your-domain>
```
#### ③Oathu＆IAPの設定
下記のサイドを参考しながら設定します。
https://cloud.google.com/iap/docs/enabling-kubernetes-howto?hl=ja
```
kubectl create secret generic <your-secret>\
 --from-literal=client_id=client_id_key \
 --from-literal=client_secret=client_secret_key
```

#### ④BackendConfig.yaml
Rstudio
```
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  name: config-rstudio
  namespace: <your-namespace>
spec:
  iap:
    enabled: true
    oauthclientCredentials:
      secretName: <your-secret>
  healthCheck:
    checkIntervalSec: 15
    timeoutSec: 15
    healthyThreshold: 1
    unhealthyThreshold: 2
    type: HTTP
    requestPath: /health
    port: <your-rstudio-port番号>
```

jupyter
```
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  name: config-jupyter
  namespace: <your-namespace>
spec:
  iap:
    enabled: true
    oauthclientCredentials:
      secretName: <your-secret>
  healthCheck:
    checkIntervalSec: 15
    timeoutSec: 15
    healthyThreshold: 1
    unhealthyThreshold: 2
    type: HTTP
    requestPath: /jupyter/login?next=%2Flab%3F 
    port: <your-jupyter-port番号>
```

#### ⑤service.yaml
Rstudio
```
apiVersion: "v1"
kind: "Service"
metadata:
  name: "rstudio-service"
  namespace: <your-namespace>
  annotations:
    beta.cloud.google.com/backend-config: '{"default": "config-rstudio"}'
spec:
  ports:
  - port: <your-rstudio-port番号>
    protocol: TCP
    targetPort: <your-rstudio-port番号>
  selector:
    app: rstudio
  type: ClusterIP
```

jupyter
```
apiVersion: "v1"
kind: "Service"
metadata:
  name: "jupyter-service"
  namespace: <your-namespace>
  annotations:
    beta.cloud.google.com/backend-config: '{"default": "config-jupyter"}'
spec:
  ports:
  - port: <your-jupyter-port番号>
    protocol: TCP
    targetPort: <your-jupyter-port番号>
  selector:
    app: jupyter
  type: ClusterIP
```
※typeはClusterIPでもNodePortIPでも大丈夫です。

#### ⑥ingress.yaml
```
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: <your-ingress>
  namespace: <your-namespace>
  annotations:
    ingressclass.kubernetes.io/is-default-class: "true"
    kubernetes.io/ingress.global-static-ip-name: "<your-static-ip>"
    networking.gke.io/managed-certificates: "<your-Certificate>"
    kubernetes.io/ingress.allow-http: "false"
spec:
  defaultBackend:
    service:
      name: rstudio-service
      port:
        number: <your-rstudio-port番号>
  rules:
  - http:
      paths:
      - path: /jupyter
        pathType: Prefix
        backend:
          service:
            name: jupyter-service
            port:
              number: <your-jupyter-port番号>
```


## phase２
元々はCloudRunを利用予定ですが、現在はGAE上にデプロイしています。
こちらの部分が自分の担当ではないで、同じGKEにデプロイした方が良いかもしれませんので、
一旦スキップさせていただきます。
また更新します。

# 四、最後
インフラの方では、terraformを使ってインフラを構築した方が良いと実感できました。
Rstudioの方については、healthcheckなど色々な問題が出てきました。
ユーザ要望によりアップデートする予定ですので、こちらの文章を何回も更新する可能性があります。