動画配信プラットフォーム ~ AWS から GCP 二刀流への道のり ~

streampack の動画配信プラットフォームの開発を担当している Tana です。

# 概要

弊社は先日 Google Cloud™ パートナー認定を取得しました。
既存サービスは AWS をベースに提供してますが、
今後`GCP`上で動画配信のニーズも増えてくることも考えられ、
この機会に知見を増やすべく、`streampack on GCP` を試みました。
その際のプロセスとトラブルシューティングなどを共有できればと思っております。

# リソース部分の構成

| リソース | AWS | GCP |
|:-----------------|:-----------------|:-----------------|
| 仮想サーバ(コンテナ) | ECS(EC2) | GKE |
| コンテナレジストリ | ECR | GCR |
| データベース | RDS(MySQL) | Cloud SQL |
| ストレージ | S3 | GCS(Storage) |
| CDN | CloudFront | Cloud CDN |
| 動画変換 | MediaConvert / ElasticTranscoder | FFmpeg on GKE |
| ログ | CloudWatch Logs | StackDriver |

GCP には動画変換のマネージドサービスはないので、FFmpeg を使って HLS に変換させます。

# アプリケーション部分

アプリケーションにて AWSへの操作は AWS Ruby SDK v3 を使用していたので、
`Aws::` というキーワードで影響範囲を洗い出し、動画コアである下記の部分の修正が
必要でした。

- アップロード
- サムネイル
- 動画変換
- 配信

他のオプショナルな機能の一つとしてライブからアーカイブファイル対応にて S3 + SQS + Lambda を使ってもいましたが今回は割愛 :bow: 

## アップロード

GCSは AWS S3 と同様に Direct Upload をサポートしておりましたので、
下記のように put signed URL を返してくれるように API を修正しました。
GCP or AWS の環境に応じてそれぞれのURLを返し、アップロード時にセットしてます。

コマンド
> $ gsutil signurl -m PUT -d 1d -c video/mp4 ~/credentials.json gs://your-bucket-name/test.mp4


``` put-signed-url.rb
storage = Google::Cloud::Storage.new
bucket = storage.bucket(ENV['GCS_BUCKET'])
sslkey = OpenSSL::PKey::RSA.new("-----BEGIN PRIVATE KEY-----\n...")

put_url = bucket.signed_url(
  s3_key,
  method: 'PUT',
  signing_key: sslkey,
  issuer: ENV['GCS_SV_ACCOUNT'],
  expires: 1.hour,
  content_type: filetype  # これ重要
)
```

signed URLのレスポンスが体感で数秒かかるので、signed URL のリストを取得したり、リクエストが多いサービスでの使用は注意が必要そうです。

## サムネイル

CarrierWave, Fog のライブラリは様々な AWS, GCP, Azure など Cloud Provider をサポートしているので、基本設定を変え同じコードでサムネイルの生成・リサイズ・配信ができます。

## 動画変換

GCPは動画配信のマネージドサービスはないので、worker デーモンプロセスを GKE の pod 上で FFmpeg を使いバックグラウンドで変換させてます。もともとローカル開発で AWS リソース設定していなくても動くことを考慮していたので、フィーチャーフラグにて ElasticTranscoder -> FFmpeg 切り替えれるようにしてます。もちろん GKE の特徴でもあるスケーラビリティなので、Node or Pods 数を増やして並列で処理することも可能です。

Adaptive Bitrate対応や透かし入れたり、プリセット定義や複数パイプラインでの実施を考慮するケースではどうしても自前でやると workflow が大変なので、要件に応じてマネージドサービスを使う方が賢明でしょう。

# GCPリソースセットアップ

## GCS(Storage)

AWS S3と同じオブジェクトストレージで概念や操作も似ているので s3 経験者であればスムーズかと思います。GCPの便利なのが、 Cloud Shell 上から、簡単に操作できるところです。わざわざ AWS CLI のセットアップや AWS IAM からcredentials を発行しローカルにセットしたり、セットアップは不要です。

GCS の操作は `gcloud` ではなく `gsutil` になります。

ヘルプ
> $ gsutil help

コピーファイル
> gsutl cp test.mp4 gs://your-bucket-name/

public対応
> $ gsutil -D setacl public-read gs://your-bucket-name/test.mp4


cors 設定は画面上からできないので、下記のコマンドで実施します。

```
$ gsutil cors set cors.json gs://<your-bucket-name>
----
[
    {
      "origin": ["https://<your domain>"],
      "responseHeader": ["*"],
      "method": ["GET", "PUT", "HEAD"],
      "maxAgeSeconds": 3600
    }
]
----
```

### クレデンシャルの発行

AWS の場合は IAM から発行しますが、GCSの場合は Storage -> Settings から発行できます。

<img width="874" alt="gcs-access-key.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232803/dbb13082-d765-e00c-4d50-75a103e503c5.png">

## Cloud SQL - データーベース

GKEからデータベース接続する際は下記の方法があります。

- public IP
- private IP
- cloud SQL Proxy

public IP だと変わってしまうことがあるため、セキュアな接続である Proxy を使った方法で実施しました。

## サービスアカウント
### クレデンシャルの発行
Owner 権限を持ったアカウントで Cloud SQL Client の Role を持ったサービスアカウントを作成します。
credentials を上記のように取得して、後ほど使うのでダウンロードしておきます。

<img width="1136" alt="service-account-with-role.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232803/865e13ce-1775-79dd-9bc2-396f1471a6fe.png">

> 注意: Owner 権限出ないと、Roleの選択画面が出てこないので注意です。


## GCR - コンテナレジストリ
ECRと違って、リポジトリの事前作成は不要です。
GCRに push できるように下記のセットアップして、ホスト先を指定して push します。

```
$ gcloud auth configure-docker
```

GCRホストが一番近そうな `asia.gcr.io` を指定してます。

```
$ docker build -t asia.gcr.io/<your-project-name>/<image name> .
$ docker push asia.gcr.io/<your-project-name>/<image_name>
```

### Vulnerability scanning(beta)

コンテナ内の脆弱性診断がベータ版ですが、提供されております。
Enable にするだけで、push後に診断してくれます。

下記はアプリケーションの結果です。
https://qiita.com/ytanaka3/items/8c308db2ee58ea63626a
にてブログ書きましたが、コンテナイメージの最適化を行っていたので、大丈夫でした。

<img width="822" alt="Container_Registry_-_streampack-gcp_-_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232803/77a8361b-20ce-ebc7-d963-dda0a15cfb32.png">

nginx だと、、、

<img width="926" alt="Container_Registry_-_streampack-gcp_-_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232803/db062312-0d0c-423c-05f7-8914aa066d5c.png">

:scream: :scream: :scream:  見直しが必要そうです。。

ミドルウェア周りのセキュリティ対策を DevOps標準フローとして導入できそうです。

## GKE - Google Kubernetes Engine

### cloudsql-proxy 設定

先ほどのサービスアカウントで設定しダウンロードした credentials.json を下記のコマンドにて Secrets として登録します。

> $ kubectl create secret generic cloudsql-instance-credentials \
--from-file=credentials.json=/path/to/credentials.json

Cloud Shell の場合は credentials のアップロードは下記の方法で可能です。

<img width="1206" alt="Kubernetes_Engine_-_streampack-gcp_-_Google_Cloud_Platform.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232803/b492019e-0fbc-7fca-8fb0-7dea1ff34c3b.png">

確認は下記のコマンドでも確認できます。

> $ kubectl get secrets

### ConfigMap 設定

環境変数を deployment or pod ファイルにそれぞれ定義することもできますが、すでに準備した .envファイルを読み込ませて、登録します。

> $ kubectl create configmap cms-config --from-env-file=.env

``` .env
DB_HOST=127.0.0.1
DB_USERNAME=xxxx
DB_PASSWORD=xxxx
DB_NAME=xxxx

GCS_BUCKET=bucket-name
GCS_KEY=xxx
GCS_SV_ACCOUNT=xxx
```

cloudsql-proxy のケースのホスト名は `127.0.0.1` になりますので注意が必要です。(public IPではないです。)

正しく登録されたか、中身の確認は下記を実行します。

> $ kubectl get configmap app-config -o yaml

### deployment 設定
> $ kubectl create -f app-deployment.yaml

``` app-deployment.yaml
      #... 抜粋

      # cloudSQL用のイメージ
      containers:
        # CMSアプリ
        - image: asia.gcr.io/<your-project-name>/<repository>
          name: app
　　　　　　# configMap で定義した環境変数読み込み
          envFrom:
            - configMapRef:
                name: cms-config
        # ... 抜粋

        # cloudsql-proxy サイドカー
        - image: b.gcr.io/cloudsql-docker/gce-proxy:1.14
          name: cloudsql-proxy
          command: ["/cloud_sql_proxy",
                    "-instances=<your-project-name>:<db-region>:<db-name>=tcp:3306",
                    "-credential_file=/secrets/cloudsql/credentials.json"]
          securityContext:
            runAsUser: 2
            allowPrivilegeEscalation: false

      # マウント
      volumes:
        - name: cloudsql-instance-credentials
          secret:
            secretName: cloudsql-instance-credentials

```

細かな設定で時間を要したのが、volumes マウントです。
上記のコードには記載してませんが、ECSだとマウントする際は、マウント元の情報はキープされますが、k8s だとマウントしたパスのファイルは削除されます。nginx 上で assets を配信していたのですが、アクセスできなかったので、

```
$ kubectl exec -it <pod name> -c <container name> sh       # <- nginx
```

にて確認すると、コンテナ内で assets 配下を見てみると空っぽでした。
調べたりテストする限りだと仕様のようなので、lifecycle:postStart にてコピーするように対応しました。

### サービス設定

サービス(Ingress)はロードバランサーであり AWS でいう ELB(ALB) に当たります。

> $ kubectl create -f app-service.yaml

``` app-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: app-lb
  labels:
    app: app
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
  selector:
    app: app
```

確認は

> $ kubectl get svc

``` ruby
NAME               TYPE           CLUSTER-IP    EXTERNAL-IP     PORT(S)          AGE
app-lb          LoadBalancer   10.4.15.244   xx.xxx.xx.xxx   80:31485/TCP     18d
```

払い出された EXTERNAL-IP にアクセスしてサービス確認します。

### DB接続がうまくいかない場合は？

StackDriverを確認します。

<img width="1570" alt="gke-containers-stackdriver.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232803/e0dc6863-f6bf-85e2-718b-f01953d3525e.png">

またはコマンドからもログを確認できます。

```
$ kubectl get pods   # pod名を取得
$ kubectl logs -f <pod name> <container name>    # cloudsql-proxy
```

### pods&service の削除

```
$ kubectl delete -f app-deployment.yaml
$ kubectl delete -f app-lb.yaml
```

## Cloud CDN

今回は静的ファイルのHLS動画ファイルは GCS に置いてますので、Cloud CDNで配信できるようにします。Cloud CDN は Load Balancing(Backend & Frontend)の組み合わせ設定が必要のようです。

### バックエンド設定
Storageがオリジンになりますので、backend には Storage を指定します。

![backend.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232803/62552479-a424-1d81-801e-6a6c21357e58.jpeg)



### フロントエンド設定
設定すると、フロントエンドのIPアドレスが発行されます。

![frontend.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232803/12eb481a-944b-1d8b-e748-e1929b3f1acb.jpeg)


### パスルール
パスルールは今回は特にルーティング不要なので、デフォルトのままにしてます。

![path-rule.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232803/77bf435b-644f-dc44-b690-f36c3dc11c46.jpeg)

下記は生成後の画面になります。

![cdn-complete.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232803/fdaf89ef-f2b2-0b01-77ce-2e2d21443384.jpeg)


とりあえず、IPアドレスが払い出されたので、そのIPと bucket にある public ファイルにアクセスできます。キャッシュのインバリデーションは AWS CloudFront に比べると数分かかりますので、サービスによっては注意が必要です。また、Cloud Interconnect を使えば、`Akamai` や `Fastly` でも配信できそうなので、マルチCDNで配信するニーズがある場合は最適かもしれません。
https://cloud.google.com/interconnect/docs/how-to/cdn-interconnect

# 結論

あくまで私がGCSのメリットを実感したことは、

- プロジェクト間のスイッチが簡単(dev -> production, project A -> project B)
- Cloud Shell にて、疎通確認やコマンドにて動作確認が可能
- デベロッパーフレンドリーな管理画面(選択肢が最低限)
- AWS に比べると Role に悩まされることが少ない。

まだ理解できていないリソースやアプリ側で対応できていないところもあり課題が山積みですが、
パブリッククラウドの知識・経験があれば GCPリソースのセットアップはスムーズに進めることができました。
アプリ側も改善し、個々のリソースの最適化を行い、スムーズにGCP上で構築&サービス提供できるように進めればと思います。