Edited at

動画配信プラットフォーム ~ 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 から発行できます。

gcs-access-key.png


Cloud SQL - データーベース

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


  • public IP

  • private IP

  • cloud SQL Proxy

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


サービスアカウント


クレデンシャルの発行

Owner 権限を持ったアカウントで Cloud SQL Client の Role を持ったサービスアカウントを作成します。

credentials を上記のように取得して、後ほど使うのでダウンロードしておきます。

service-account-with-role.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

にてブログ書きましたが、コンテナイメージの最適化を行っていたので、大丈夫でした。

Container_Registry_-_streampack-gcp_-_Google_Cloud_Platform.png

nginx だと、、、

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

Kubernetes_Engine_-_streampack-gcp_-_Google_Cloud_Platform.png

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


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

gke-containers-stackdriver.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


フロントエンド設定

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

frontend.jpg


パスルール

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

path-rule.jpg

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

cdn-complete.jpg

とりあえず、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上で構築&サービス提供できるように進めればと思います。