MediaPackage 用の CloudFront ディストリビューションを AWS SDK で作成する

2020_12

はじめに

とある事情で MediaPackage のエンドポイント用の CloudFront ディストリビューションを AWS SDK で作成する機会がありました。その際得た知見をソースコードを交えながら備忘録として記事に残しておきます。

本記事内容で紹介しているソースコードは Gist にも同じ内容でアップしてあります。

ちなみに MediaLive + MediaPackage + CloudFront の構成でインフラ構築したい場合は、CloudFormation が MediaPackage にも対応したので CloudFormation の利用を推奨します。

本記事内容はあくまでも何らかの事情で、後から CloudFront ディストリビューションを MediaPackage エンドポイントに紐づけたいケース等で参考になると思われます。

実装内容

作成したソースコードの内容は下記になります。
最下部の createDistributionForMediaPackage が本記事タイトルに該当する関数です。

CloudFrontClientForMediaPackage.ts
import { CloudFront } from "aws-sdk";
import * as url from "url";
import {
    CreateDistributionWithTagsResult,
    GetDistributionResult,
    UpdateDistributionResult
} from "aws-sdk/clients/cloudfront";
export class CloudFrontClientForMediaPackage {
    private cloudFront: CloudFront;
  constructor() {
    this.cloudFront = new CloudFront({
        region: "ap-northeast-1",
        apiVersion: '2020-05-31',
    });
}
/**
 * CloudFront ディストリビューションの情報を取得するために利用する
 * @param id CloudFront ディストリビューションの ID
 * @return ディストリビューションの情報を取得する
 */
  async getDistribution(id: string): Promise<GetDistributionResult> {
      const distribution = await this.cloudFront.getDistribution({
          Id: id
      }).promise()
      return distribution;
  }
  /**
   * CloudFront ディストリビューションの設定内容を取得するために利用する
   * @param id CloudFront ディストリビューションの ID
   * @return ディストリビューションの設定内容を取得する
   */
  async getDistributionConfig(id: string): Promise<CloudFront.DistributionConfig> {
      const config = await this.cloudFront.getDistributionConfig({
          Id: id
      }).promise()
      return config.DistributionConfig;
  }
  /**
   * CloudFront ディストリビューションを削除する
   * @param id 削除したい CloudFront ディストリビューションの ID
   */
  async deleteDistribution(id: string) {
    const distribution = await this.getDistribution(id);
    await this.cloudFront.deleteDistribution({
        Id: id, IfMatch: distribution.ETag
    }).promise()
  }
  /**
   * CloudFront ディストリビューションを無効化する
   * @param id 無効化したい CloudFront ディストリビューションの ID
   * @return 無効化した CloudFront ディストリビューションの情報
   */
  async disableDistribution(id: string): Promise<UpdateDistributionResult> {
      const distribution = await this.getDistribution(id);
      const config = distribution.Distribution.DistributionConfig;
      config.Enabled = false;
      return await this.cloudFront.updateDistribution({
        Id: id,
        IfMatch: distribution.ETag,
        DistributionConfig: config
      }).promise();
  }
  /**
   * MediaPackage のエンドポイント用の CloudFront ディストリビューションを作成する
   * @param id CloudFront ディストリビューションを判別するための ID
   * @param mediaPackageArn MediaPackage チャンネルの ARN
   * @param mediaPackageUrl MediaPackage エンドポイントの URL
   */
  async createDistributionForMediaPackage(
      id: string,
      mediaPackageArn: string,
      mediaPackageUrl: string
    ): Promise<CreateDistributionWithTagsResult> {
    // 1. url モジュールを用いて URL 文字列をパースする
    const mediaPackageEndpoint = url.parse(mediaPackageUrl);
    /**
    2. MediaPackage のエンドポイント URL から FQDN を取得する。
    後述する CloudFront ディストリビューションのオリジンのドメイン名としても利用する
    */
    const mediaPackageHostname = mediaPackageEndpoint.hostname;
    /**
    3. MediaPackage のエンドポイント URL のフォーマットは
    https://<AccountID>.mediapackage.<Region>.amazonaws.com/**** となっているので、
    FQDN の先頭部分を文字列分割で取り出すとアカウント ID が取得できる
    */
    const accountId = mediaPackageHostname.split('.')[0];
    // 4. 後述する CloudFront ディストリビューションのオリジン ID として、アカウント ID を利用する
    const targetOriginId = `MP-${accountId}`
    /**
    5. createDistribution ではなく、createDistributionWithTags 関数で、
    CloudFront ディストリビューションを作成する。MediaPackage との紐付けにタグを利用するため。
    */
    return await this.cloudFront.createDistributionWithTags({
        DistributionConfigWithTags: {
            Tags: {
                Items: [
                    /**
                    !!!!!重要!!!!!
                    6. CloudFront ディストリビューションに紐付けたい
                    MediaPackage エンドポイントのチャンネル ARN を
                    mediapackage:cloudfront_assoc で定義する。
                    mediapackage:cloudfront_assoc を定義することで、
                    CloudFront ディストリビューションと
                    MediaPackage チャンネルを紐付けることが可能となる。
                    */
                    {
                        Key: 'mediapackage:cloudfront_assoc',
                        Value: mediaPackageArn
                    },
                    {
                        Key: 'Id',
                        Value: id
                    },
                    {
                        Key: 'Product',
                        Value: 'product'
                    },
                    {
                        Key: 'Stage',
                        Value: 'dev'
                    }
                ]
            },
            DistributionConfig: {
                CallerReference: new Date().toISOString(),
                Comment: `Managed by MediaPackage - ${id}`,
                Enabled: true,
                /**
                7. CloudFront ディストリビューションのオリジンには 2つ設定します。
                1つが MediaPackage のエンドポイントに対するものと、
                もう 1つが MediaPacakge サービスに対するものです。
                基本的には MediaPackage のエンドポイントに対するオリジンを利用します。
                例外時に向けるオリジンが MediaPacakge サービスに対するものになります。
                */
                Origins: {
                    Quantity: 2,
                    Items: [
                        {
                            DomainName: mediaPackageHostname,
                            Id: targetOriginId,
                            CustomOriginConfig: {
                                HTTPPort: 80,
                                HTTPSPort: 443,
                                OriginProtocolPolicy: 'match-viewer'
                            }
                        },
                        {
                            DomainName: 'mediapackage.amazonaws.com',
                            Id: "TEMP_ORIGIN_ID/channel",
                            CustomOriginConfig: {
                                HTTPPort: 80,
                                HTTPSPort: 443,
                                OriginProtocolPolicy: 'match-viewer'
                            }
                        }
                    ]
                },
                /**
                8. CacheBehaviors のいずれにも当てはまらなかった場合の
                キャッシュの振る舞いを定義します。
                MediaPackage は タイムシフト表示機能を使用する際等で、クエリ文字列に start, m, end を利用しています。
                そのため、それらの文字列は WhitelistedNames に含め QueryString には true を指定しておきます。
                DefaultCacheBehavior に引っかかる挙動は例外的扱いなので、
                使用するオリジンは MediaPackage サービスのものを設定します。
                */
                DefaultCacheBehavior: {
                    ForwardedValues: {
                        Cookies: {
                            Forward: 'whitelist',
                            WhitelistedNames: {
                                Quantity: 3,
                                Items: [
                                    'end', 'm', 'start'
                                ]
                            }
                        },
                        QueryString: true,
                        Headers: {
                            Quantity: 0
                        },
                        QueryStringCacheKeys: {
                            Quantity: 0
                        }
                    },
                    MinTTL: 6,
                    TargetOriginId: "TEMP_ORIGIN_ID/channel",
                    TrustedSigners: {
                        Enabled: false,
                        Quantity: 0
                    },
                    ViewerProtocolPolicy: 'redirect-to-https',
                    AllowedMethods: {
                        Items: [
                            'GET', 'HEAD'
                        ],
                        Quantity: 2,
                    },
                    MaxTTL: 60
                },
                /**
                9. CloudFront のエラーコード全ての TTL に 1sec を設定します。
                MediaPackage のエラーのキャッシュが長時間持続してしまうと、
                その間は MediaPackage で正常に配信できているとしても、
                復旧できない状態となるからです。
                */
                CustomErrorResponses: {
                    Quantity: 10,
                    Items: [
                    {
                        ErrorCode: 400,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 403,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 404,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 405,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 414,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 416,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 500,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 501,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 502,
                        ErrorCachingMinTTL: 1
                    }, {
                        ErrorCode: 503,
                        ErrorCachingMinTTL: 1
                    }
                    ]
                },
                /**
                10. CloudFront ディストリビューションのキャッシュの振る舞いを 2つ定義します。
                それぞれの設定内容は基本的に DefaultCacheBehavior で定義したものと同様です。
                しかし、利用するオリジンは MediaPackage エンドポイントに向けたものを利用します。
                1つは Microsoft Smooth Streaming での配信時に利用する
                index.ism に対するもので Smooth Streaming を true に設定しています。
                もう 1つは上記 Microsoft Smooth Streaming 以外の
                全てに当てはまるストリーミングに適用されるものになります。
                */
                CacheBehaviors: {
                    Quantity: 2,
                    Items: [{
                        MinTTL: 6,
                        PathPattern: 'index.ism/*',
                        TargetOriginId: targetOriginId,
                        ViewerProtocolPolicy: 'redirect-to-https',
                        AllowedMethods: {
                            Items: [
                                'GET', 'HEAD'
                            ],
                            Quantity: 2,
                        },
                        ForwardedValues: {
                            Cookies: {
                                Forward: 'whitelist',
                                WhitelistedNames: {
                                    Quantity: 3,
                                    Items: [
                                        'end', 'm', 'start'
                                    ]
                                }
                            },
                            QueryString: true,
                            Headers: {
                                Quantity: 0
                            },
                            QueryStringCacheKeys: {
                                Quantity: 0
                            },
                        },
                        SmoothStreaming: true
                    }, {
                        MinTTL: 6,
                        PathPattern: '*',
                        TargetOriginId: targetOriginId,
                        ViewerProtocolPolicy: 'redirect-to-https',
                        AllowedMethods: {
                            Items: [
                                'GET', 'HEAD'
                            ],
                            Quantity: 2,
                        },
                        ForwardedValues: {
                            Cookies: {
                                Forward: 'whitelist',
                                WhitelistedNames: {
                                Quantity: 3,
                                Items: [
                                    'end', 'm', 'start'
                                ]
                                }
                            },
                            QueryString: true,
                            Headers: {
                                Quantity: 0
                            },
                            QueryStringCacheKeys: {
                                Quantity: 0
                            },
                        }
                    }]
                },
                PriceClass: 'PriceClass_All'
            }
        }
    }).promise()
  }
}

createDistributionForMediaPackage で作成したディストリビューションは、公式ページに記載された手順 で作成した CloudFront ディストリビューションと同等のものになります。

詳細な説明はインラインコメントにて書きましたが、一応補足説明を少し付け加えておきます。

随所に出てくる Quantity について

Quantity には Items で指定する項目の数を入力します。 例えば HeadersQueryStringCacheKeys には Items に何も指定していないため、Quantity0 を指定します。

しかし、AllowedMethodsWhitelistedNames には Items に指定した項目数である 23Quantity に入力しています。Quantity の数と Items の項目数が合わないと、エラーが発生するため、注意が必要です。

mediapackage:cloudfront_assoc を定義する意味

CloudFront ディストリビューションのタグに mediapackage:cloudfront_assoc で紐付ける MediaPackage のチャンネル ARN を指定することで、MediaPackage コンソールから紐付けられた CloudFront ディストリビューション情報を参照できるようになります。

試しに紐づけられた MediaPackage のチャンネルのエンドポイント詳細ページに遷移すると、
下記のような画面が確認できるはずです。

mediapackage:cloudfront_assoc で紐付いた CloudFront ディストリビューションが確認できる
mediapackage:cloudfront_assoc で紐付いた CloudFront ディストリビューションが確認できる

なお、本記事内のソースコードでは他にも Id, Product, Stage といったタグを定義していますが、MediaPackage とは関係無いものなので削除して問題ありません。

updateDistribution を実行する際の注意点

これは今回の記事内容とは直接関係ないのですが、地味にハマったので載せておきます。

CloudFront では createDistribution の時に要求されるパラメータよりも updateDistribution で要求されるパラメータのほうが多いです。 AWS 公式ページの比較表にある通りです。

そのため、updateDistribution で設定を一部更新したいだけなのに、とても多くのパラメータを指定する必要があり非常に面倒です。例えば CloudFront ディストリビューションの Enable/Disable を切り替えるだけでも 30 個近いパラメータを指定する必要あります。

上記の入力の手間を省くのには getDistribution で取得した既存のディストリビューション情報を改変する形で updateDistribution のパラメータを作成すると楽でした。

今回のソースコードの内容を参照すると disableDistribution が該当します。

// 1. getDistribution を実行して CloudFront ディストリビューションの情報を取得する
const distribution = await this.getDistribution(id);
// 2. CloudFront ディストリビューションの設定内容を取得する
const config = distribution.Distribution.DistributionConfig;
// 3. CloudFront ディストリビューションの Enabled/Disabled を切り替えるオプションを改変する
config.Enabled = false;
// 4. 3. で改変した内容を updateDistribution で CloudFront ディストリビューションに反映する
return await this.cloudFront.updateDistribution({
    Id: id,
    IfMatch: distribution.ETag,
    DistributionConfig: config
}).promise();

おわりに

ニッチな内容なので、本記事内容を今後利用するかどうかは分かりませんが、一応得た知見を記事として残しておきました。同様のことを行う必要が出てきた方の参考になれれば幸いです。

参考リンク