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

概要

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

リソース部分の構成

リソース AWS GCP
仮想サーバ(コンテナ) ECS(EC2) GKE
コンテナレジストリ ECR GCR
データベース RDS(MySQL) CloudSQL
ストレージ 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 から発行できます。

CloudSQL – データーベース

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

  • public IP
  • cloudSQL Proxy

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

サービスアカウント

クレデンシャルの発行

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

注意: 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
にてブログ書きましたが、コンテナイメージの最適化を行っていたので、大丈夫でした。

nginx だと、、、

: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 のアップロードは下記の方法で可能です。

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

$ kubectl get secrets

ConfigMap 設定

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

$ kubectl create configmap cms-config –from-env-file=.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

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を確認します。

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

$ 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 を指定します。

フロントエンド設定

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

パスルール

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

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

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

結論

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

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

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

元記事はこちら

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