とある事情で 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 には Items で指定する項目の数を入力します。 例えば Headers や QueryStringCacheKeys には Items に何も指定していないため、Quantity に 0 を指定します。
しかし、AllowedMethods や WhitelistedNames には Items に指定した項目数である 2 や 3 を Quantity に入力しています。Quantity の数と Items の項目数が合わないと、エラーが発生するため、注意が必要です。
CloudFront ディストリビューションのタグに mediapackage:cloudfront_assoc で紐付ける MediaPackage のチャンネル ARN を指定することで、MediaPackage コンソールから紐付けられた CloudFront ディストリビューション情報を参照できるようになります。
試しに紐づけられた MediaPackage のチャンネルのエンドポイント詳細ページに遷移すると、
下記のような画面が確認できるはずです。
mediapackage:cloudfront_assoc で紐付いた CloudFront ディストリビューションが確認できる
なお、本記事内のソースコードでは他にも Id, Product, Stage といったタグを定義していますが、MediaPackage とは関係無いものなので削除して問題ありません。
これは今回の記事内容とは直接関係ないのですが、地味にハマったので載せておきます。
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();
ニッチな内容なので、本記事内容を今後利用するかどうかは分かりませんが、一応得た知見を記事として残しておきました。同様のことを行う必要が出てきた方の参考になれれば幸いです。