AWS CDKのコードに仕込んでおくと便利なバリデーション関数
株式会社MIXIでサロンスタッフ予約サービス「minimo」の機械学習やバックエンドの開発をしている鈴木です。
今回はAWS CDKのコードに仕込んでおくと便利なバリデーション関数をいくつか紹介します。
バリデーション関数を仕込む利点
AWS CDKはAWS CloudFormationのラッパーという側面があるため、それ自体のバリデーションはあまり充実していません。
そのため、デプロイ時に失敗したり、実際に運用してみてミスに気づいたりする場面が存在します。
そこでバリデーション関数を用意し、Constructで実行するようにすると、設定をミスした場合、 cdk diff が失敗するようになります。
これにより、デプロイ前にミスに気づきやすくなります。
バリデーション関数
EC2のインスタンスタイプが対応しているアベイラビリティーゾーンを検査する
ap-northeast-1 リージョンには ap-northeast-1a, ap-northeast-1c, ap-northeast-1d の3つのアベイラビリティーゾーンが存在します。
しかし、EC2のインスタンスタイプの中にはそれら全てに対応していないものも存在します。
このようなインスタンスタイプを使ってECS on EC2を行うと、ECSタスクのデプロイに成功するかどうかがデプロイされるアベイラビリティーゾーンによってしまい、不安定になってしまいます。
このような事態を防ぐため、使用しているインスタンスタイプが全てのアベイラビリティーゾーンに対応しているかを確認するバリデーション関数があると便利です。
AWS APIの DescribeInstanceTypeOfferings を使うと、あるアベイラビリティーゾーンで使えるインスタンスタイプの一覧を取得できます。
Filters でインスタンスタイプを絞ることで、あるインスタンスタイプに対応するアベイラビリティーゾーンの一覧を出す使い方をしています。
import { DescribeInstanceTypeOfferingsCommand } from '@aws-sdk/client-ec2';
import type { EC2Client } from '@aws-sdk/client-ec2';
export const validateEC2InstanceType = async (client: EC2Client, instanceType: string, region: string) => {
const response = await client.send(
new DescribeInstanceTypeOfferingsCommand({
LocationType: 'availability-zone',
Filters: [
{
Name: 'instance-type',
Values: [instanceType],
},
],
})
);
if (response.InstanceTypeOfferings === undefined) {
throw new Error(`Instance type ${instanceType} is not available in region ${region}`);
}
const availabilityZones = response.InstanceTypeOfferings.map((it) => it.Location).filter((it) => it !== undefined);
const availabilityZoneSet = new Set(availabilityZones);
const expectedAvailabilityZones = ['ap-northeast-1a', 'ap-northeast-1c', 'ap-northeast-1d'];
// EC2インスタンスタイプがap-northeast-1a, ap-northeast-1c, ap-northeast-1dの全てに対応していない場合
if (!expectedAvailabilityZones.every((az) => availabilityZoneSet.has(az))) {
const availabilityZonesStr = availabilityZones.join(', ');
throw new Error(`Instance type ${instanceType} is only available in availability zones ${availabilityZonesStr}`);
}
};
ECS on Fargateにおける、ECSタスクのCPUとメモリの組み合わせを検査する
上記に記載されているように、ECS on FargateではECSタスクのCPUとメモリの値は決められた組み合わせに則っている必要があります。
これを覚えるのは大変なので、満たしているか確認するバリデーション関数があると便利です。
なお、この組み合わせをAWSのAPIで取得することはできないようだったので、上記ページの内容をそのままコードに起こす形で実装しています。
export const validateFargateCPUAndMemory = (memory: number, cpu?: number) => {
switch (cpu) {
case 256:
// memoryLimitMiBが512 MB, 1 GB, 2 GBのいずれかか
if (![512].concat([...Array(2)].map((_, i) => (i + 1) * 1024)).includes(memory)) {
throw new Error('cpuとmemoryLimitMiBが正しく指定されていません');
}
break;
case 512:
// memoryLimitMiBが1 GB, 2 GB, 3 GB, 4 GBのいずれかか
if (![...Array(4)].map((_, i) => (i + 1) * 1024).includes(memory)) {
throw new Error('cpuとmemoryLimitMiBが正しく指定されていません');
}
break;
case 1024:
// memoryLimitMiBが2 GB, 3 GB, 4 GB, 5 GB, 6 GB, 7 GB, 8 GBのいずれかか
if (![...Array(7)].map((_, i) => (i + 2) * 1024).includes(memory)) {
throw new Error('cpuとmemoryLimitMiBが正しく指定されていません');
}
break;
case 2048:
// memoryLimitMiBが4 GB ~ 16 GB (1 GB のインクリメント) のいずれかか
if (![...Array(13)].map((_, i) => (i + 4) * 1024).includes(memory)) {
throw new Error('cpuとmemoryLimitMiBが正しく指定されていません');
}
break;
case 4096:
// memoryLimitMiBが8 GB ~ 30 GB (1 GB のインクリメント) のいずれかか
if (![...Array(23)].map((_, i) => (i + 8) * 1024).includes(memory)) {
throw new Error('cpuとmemoryLimitMiBが正しく指定されていません');
}
break;
case 8192:
// memoryLimitMiBが16 GB~60 GB (4 GB のインクリメント) のいずれかか
if (![...Array(12)].map((_, i) => (4 * i + 16) * 1024).includes(memory)) {
throw new Error('cpuとmemoryLimitMiBが正しく指定されていません');
}
break;
case 16384:
// memoryLimitMiBが32 GB~120 GB (8 GB のインクリメント) のいずれかか
if (![...Array(12)].map((_, i) => (8 * i + 32) * 1024).includes(memory)) {
throw new Error('cpuとmemoryLimitMiBが正しく指定されていません');
}
break;
default:
throw new Error('cpuが正しく指定されていません。');
}
};
ECRリポジトリのDockerイメージが存在するかを検査する
ECSタスクのコンテナにおいて、ECRリポジトリ内のDockerイメージを使う際、存在しないイメージタグを指定するとデプロイに失敗します。
これを防ぐため、ECRリポジトリのDockerイメージが存在するかを確認するバリデーション関数があると便利です。
AWS APIの DescribeImages を使うとECRリポジトリ内のDockerイメージの情報を取得できるので、それを使って実装しています。
import { ECRClient, DescribeImagesCommand, ImageNotFoundException, RepositoryNotFoundException } from '@aws-sdk/client-ecr';
export const validateECRImage = async (repositoryName: string, region: string, imageTag?: string) => {
const ecrClient = new ECRClient({ region });
try {
await ecrClient.send(
new DescribeImagesCommand({
repositoryName: repositoryName,
filter: {
tagStatus: 'TAGGED',
},
imageIds: [
{
imageTag: imageTag,
},
],
})
);
} catch (err) {
if (err instanceof ImageNotFoundException) {
throw new Error(`Dockerイメージ ${repositoryName}:${imageTag} はECRに存在しません。`);
} else if (err instanceof RepositoryNotFoundException) {
throw new Error(
`Dockerリポジトリ ${repositoryName} はECRに存在しません。Dockerリポジトリを予め作成しておく必要があります。`
);
}
}
};
宣伝
minimoでは一緒に働く仲間を募集中です!
リモート勤務も可能ですので、詳しくは下記採用ページをご確認ください。
Discussion