【日本初導入】 AWS Outposts ラックを徹底解説 第2回 〜AWS CDKによるInfrastructure as Code〜

はじめに

こんにちは、イノベーションセンターの福田です。 前回 に引き続き、 AWS Outposts について紹介していきます。

今回は、特に AWS CDK による AWS Outposts の Infrastructure as Code (以降 IaC) を行う方法について記載していきたいと思います。

AWS CloudFormation

AWS CDK では AWS CloudFormation というサービスをバックエンドで利用しているため、まずはこの AWS CloudFormation について解説します。

AWS CloudFormation は、AWS の各種リソースを JSON/YAML 形式で記述した"テンプレート"として管理します。 これによって AWS リソースの IaC を実現してくれますが、サービスとして IaC を実現するものというよりは AWS リソースの管理を AWS がマネージしてくれるものといった方が近いです。 料金はデプロイされた各種リソースにかかる料金のみで、 CloudFormation の利用自体に料金はかかりません。

CloudFormation では自動で依存関係等を解決し、適切な形でリソースのライフサイクルを管理してくれます。 デプロイされたリソース群は スタック という単位で管理されます。 このスタックへ、リソースを新しく追加したり、削除したりしても CloudFormation が依存するものの管理をおこなってくれます。 これによってユーザ側で Web コンソールや AWS CLI 等で頑張ってリソースの状態等を管理することから解放されます。 さらに、デプロイに失敗しても以前に成功してくれた状態にロールバックしてくれるため、自前で AWS のリソースのデプロイに失敗したから自分で再度構築しなおすということも行う必要はありません。

AWS CDK

AWS CDK とは AWS Cloud Development Kit の略です1。 AWS CDK 自体は一般的なプログラミング言語(TypeScript, Python, Java 等)で AWS リソースを定義・管理できるオープンソースのフレームワークです。 最終的には AWS CloudFormation のテンプレートに直り、そのテンプレートを通して各種リソースを AWS CloudFormation へプロビジョニングします。

AWS CloudFormation では JSON/YAML でテンプレートを記載していく関係上、定義が長くなりがちです。 一方、 AWS CDK はテンプレートを汎用のプログラミング言語で定義できるため、非常に短いコードで CloudFormation テンプレートを定義できます。 また、プログラミング言語を使って動的に設定できる値を自動で解決してくれるため、全てを自分で書いて定義しなければならない CloudFormation よりもコードを短くできます。

f:id:NTTCom:20220315150018p:plain

AWS CDK では各種リソースを コンストラクター というもので定義します。 コンストラクターとは要は各種リソースに対応した CDK 上のコンポーネントになります。 このコンストラクターは主に2つに分けられます2

  • ローレベルコンストラクター
    • CloudFormation のテンプレートにおけるリソースと一対一対応したコンストラクターです。
  • ハイレベルコンストラクター
    • ローレベルコンストラクターを利用しやすくラップしたり、各種リソース配下で利用するリソース(たとえば VPC における subnet 等)も一緒に簡単な設定値を指定すると自動でそれに必要なリソースも一緒に定義してくれたりしてくれるより抽象度の高いコンストラクターです。
      • 例えば aws-cdk-lib/aws-ec2Vpc コンストラクターは subnetConfiguration というプロパティに subnet の設定を入れると subnet の定義や、そこに展開される NAT の定義等も個別に定義せずともまとめて行ってくれます。
    • ローレベルコンストラクターありきなので、ローレベルコンストラクターがある機能に対応してなければハイレベルコンストラクターも対応していません。

基本的にはハイレベルコンストラクターを利用し、ハイレベルコンストラクターで対応できない事情があればローレベルコンストラクターを利用します3

現在、 v1 と v2 がそれぞれ存在している状態なのですが、ここでは v2 ベースにして解説していきます。

カスタムリソース

現在、 AWS Outposts 上に展開する一部リソースは CloudFormation では未対応な部分があるため、 CloudFormation の カスタムリソース という機能を利用する必要があります。 ここでは本題へ入る前に、今回使用するカスタムリソースを解説します。

カスタムリソースとは CloudFormation におけるリソースのカスタムプロビジョニングロジックを定義する方法です。 例えば、

  • CloudFormation が対応してないリソースを CloudFormation テンプレートで管理したい
  • CloudFormation が対応してない設定をリソースに定義したい

といったような場合に利用し、 CloudFormation で対応してないリソースでもテンプレート上で管理できるようになります。

実はカスタムリソースの実体は Lambda の関数です。 定義しておいた Lambda の関数をスタックのプロビジョニング時、つまりスタック上の他のリソースがプロビジョニングされているときに呼ぶことでリソースを作成/更新/修正する AWS API を呼び出し、リソースのプロビジョニングを行う仕組みです。 これによって、 AWS API が提供されているものの CloudFormation に対応してない新サービスのリソースをプロビジョニングすることを実現しています。

AWS CDK でもこの CloudFormation のカスタムリソースに対応しています。 特に、 AWS CDK ではカスタムリソースを利用しやすくする Provider Framework があるため、簡単にカスタムリソースを提供できます。 AWS CDK のリファレンスではいくつかの カスタムリソース実装例 が提供されていますので、詳細はそちらをご確認ください。

ここではカスタムリソースを定義する Provider Framework について少し解説しておきます。 カスタムリソースを利用するには、まずリソースの管理する Lambda 関数を定義します。 この際、 AWS API を呼び出す場合は、その API と操作対象のリソースへの操作許可ポリシーを定義しておく必要があります。 これは Function コンストラクターinitialPolicy プロパティー で定義します。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';

export class Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    // ...

    const handler = new lambda.Function(this, "Handler", {
      // ...
      initialPolicy: [
        new iam.PolicyStatement({
          resources: ["*"],
          actions: [
            // ここに Lambda で扱うリソースを操作する API に対応したアクションを追加する。
          ],
        }),
      ],
    });
  }
}

ハンドラーを定義したあとは次のようなボイラープレートコードを生成しておけばカスタムリソースの定義は完了です。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as cr from 'aws-cdk-lib/custom-resources';
import * as cdk from 'aws-cdk-lib';

export class Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    // ...

    const handler = new lambda.Function(this, "Handler", {
      // ...
    });
    const provider = new cr.Provider(this, "Provider", {
      onEgventHandler: handler,
    });
    const resource = new cdk.CustomResource(this, "Resource", {
      serviceToken: provider.serviceToken,
    });
  }
}

次に、カスタムリソースのライフサイクルを管理する Lambda 関数を作成していきます。 Lambda 関数は作成/更新/削除時に呼ばれますが、これらのイベントの種別は Lambda 関数の引数に RequestType というプロパティとしてわたってきます。 なので、 Lambda 関数を次のような形で記載してあげるとリソースを管理できます4

import * as lambda from 'aws-lambda';

const createHandler = async (event: lambda.CloudFormationCustomResourceCreateEvent): Promise<lambda.CloudFormationCustomResourceResponse> => {
  // ここは後で実装します
}
const updateHandler = async (event: lambda.CloudFormationCustomResourceUpdateEvent): Promise<lambda.CloudFormationCustomResourceResponse> => {
  // ここは後で実装します
}
const deleteHandler = async (event: lambda.CloudFormationCustomResourceDeleteEvent): Promise<lambda.CloudFormationCustomResourceResponse> => {
  // ここは後で実装します
}

export const handler = async (event: lambda.CloudFormationCustomResourceEvent): Promise<lambda.CloudFormationCustomResourceResponse> => {
  switch (event.RequestType) {
    case 'Create':
      return createHandler(event);
    case 'Update':
      return updateHandler(event);
    case 'Delete':
      return deleteHandler(event);
    default:
      throw new Error("unreachable");
  }
}

このように定義した Lambda 関数から リンク先 のようなレスポンスを返すと、 CloudFormation がその結果を利用してどのようなリソースを管理しているのかを把握します。 各種レスポンス値の意味は次の通りです。

  • Status: リソースの作成に成功したかを表すステータスです。ここは SUCCESS を固定で指定します。
  • LogicalReousrceId: CloudFormation の Logical Resource ID を指定します。これは event.LogicalResourceId として渡ってくるので、これをそのまま指定します
  • PhysicalResourceId: 実際に作成されたリソースにつけられる ID や ARN 等、そのリソースを一意に表現する値を指定します(基本的にはリソースの ID か ARN を指定することになります)
  • RequestId: Lambda の実行がリクエストされた際に付与される ID です。これは event.RequestId として既に渡ってきているので、これをそのまま指定します
  • StackId: デプロイを管理しているスタックの ID を指定します。これは event.StackId として既に渡ってきているので、これをそのまま指定します

各種ステータスや作成したいリソースの設定値等は event.ResourceProperties というプロパティに詰めらています。

import * as lambda from 'aws-lambda';

/**
 * Lambda 実行時に例外が出だときに CloudFormation 側でエラーを補足してスタックの
 * ロールバックを行ってくれます。
 *
 * 実はドキュメントだとエラーがおこった際は FAILED を Status に設定したレスポンスを
 * 返すよう指示がされていますが、これを丁寧に守って返すとエラーが発生しても CloudFormation 側は
 * エラーがおこったことを無視してデプロイに成功したとします。
 *
 * ref: https://docs.aws.amazon.com/cdk/api/v1/docs/custom-resources-readme.html#handling-lifecycle-events-onevent
 */

const createHandler = async (event: lambda.CloudFormationCustomResourceCreateEvent): Promise<lambda.CloudFormationCustomResourceResponse> => {
  // ResourceProperties にリソースの設定値が辞書形式で格納される
  const properties = event.ResourceProperties;

  // ここで AWS API を呼び出す等してリソースを作成する。

  return {
    Status: "SUCCESS",
    LogicalResourceId: event.LogicalResourceId,
    PhysicalResourceId: "<ここには作成したリソースに付くユニークな ID や ARN を指定する>",
    RequestId: event.RequestId,
    StackId: event.StackId,
  };
}
const updateHandler = async (event: lambda.CloudFormationCustomResourceUpdateEvent): Promise<lambda.CloudFormationCustomResourceResponse> => {
  // ResourceProperties にリソースの設定値が辞書形式で格納される。
  // もし、 Update が失敗した場合は以前の設定値が ResourceProperties に保存してくるので、これを元に既存のリソースをつくりなおすことになる。
  const properties = event.ResourceProperties;

  // ここでリソースを作り直す

  return {
    Status: "SUCCESS",
    LogicalResourceId: event.LogicalResourceId,
    // ドキュメントによると新しい Physical Resource ID を指定すると
    // 更に Delete イベントが走ってリソースの削除を行おうとする。
    //
    // ref: https://docs.aws.amazon.com/cdk/api/v1/docs/custom-resources-readme.html#handling-lifecycle-events-onevent
    PhysicalResourceId: "<作り直したリソースに付いている ID や ARN>",
    RequestId: event.RequestId,
    StackId: event.StackId,
  };
}
const deleteHandler = async (event: lambda.CloudFormationCustomResourceDeleteEvent): Promise<lambda.CloudFormationCustomResourceResponse> => {
  const properties = event.ResourceProperties;

  // ここでリソースを削除する

  return {
    Status: "SUCCESS",
    LogicalResourceId: event.LogicalResourceId,
    PhysicalResourceId: event.PhysicalResourceId,
    RequestId: event.RequestId,
    StackId: event.StackId,
  };
}

もし付加的な情報を返す場合、 lambda.CloudFormationCustomResourceResponseData というプロパティを利用します。 Data に辞書形式で値とキーを指定しておけば、後で CloudFormation にある Fn::GetAttr 関数5 へキーを指定することで Data に指定したキーを持つ値を参照できます。 たとえば subnet を作成した上でその ID を後で参照しようとしたとき、まずは Lambda から Data プロパティを介して値を参照できるようにします(ここでは SubnetId というキーとします)。

import * as lambda from 'aws-lambda';

const createHandler = async (event: lambda.CloudFormationCustomResourceCreateEvent): Promise<lambda.CloudFormationCustomResourceResponse> => {
  // ResourceProperties にリソースの設定値が辞書形式で格納される
  const properties = event.ResourceProperties;

  // ここで AWS API を呼び出す等してリソースを作成する。

  return {
    Status: "SUCCESS",
    LogicalResourceId: event.LogicalResourceId,
    PhysicalResourceId: "<ここには作成したリソースに付くユニークな ID や ARN を指定する>",
    RequestId: event.RequestId,
    StackId: event.StackId,
    Data: {
      SubnetId: "作成された subnet の ID",
    }
  };
}

これを CDK 側で参照する場合は次のようにします。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';

export class Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    // ...

    const resoure = new cdk.CustomResource(this, "CustomResource", { /* ... */ });
    const subnetId = resource.getAtt("SubnetId"); // Data に登録された SubnetId の値を取得
  }
}

ただし、 getAtt は AWS CDK 上だと文字列ではく aws-cdk-libIResolvable 型として返ってきます。 返ってきた値を文字列として参照したい場合は次のように aws-cdk-libtoken.asString メソッドを噛まして変換をしておく必要があります6

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';

export class Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    // ...

    const resource = new cdk.CustomResource(this, "CustomResource", { /* ... */ });
    const subnetId = cdk.Token.asString(resource.getAtt("SubnetId"));
  }
}

一方で、ライフサイクルを管理する Lambda 関数を呼び出すとき、引数を渡したい場合があると思います。 たとえば、 ALB を作成/更新/削除するカスタムリソースを作成したとします。 すると、 CreateLoadBalancer API には ALB につける名前を定義する必要がありますが、これを動的に CDK で管理したい場合などが該当します。 このような場合、 aws-cdk-libCustomResource にある properties というプロパティを使用します。 具体的には、次のように properties へ辞書形式で設定値を Lambda 関数へ流し込むことになります。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as cr from 'aws-cdk-lib/custom-resources';
import * as cdk from 'aws-cdk-lib';

export class Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    // ...

    const name = "<ALB の名前>";
    const handler = new lambda.Function(this, "Handler", {
      // ...
    });
    const provider = new cr.Provider(this, "Provider", {
      onEgventHandler: handler,
    });
    const resource = new cdk.CustomResource(this, "Resource", {
      serviceToken: provider.serviceToken,
      // properties に辞書形式で引数を登録します。
      // ここでは例として、 subnet id を `subnetId` という引数名で渡しています。
      properties: {
        name: name,
      },
    });
  }
}

すると、 Lambda 関数では次のように properties の値を参照できます。

import * as lambda from 'aws-lambda';

export const handler = async (event: lambda.CloudFormationCustomResourceEvent): Promise<lambda.CloudFormationCustomResourceResponse> => {
  // event に ResourceProperties というプロパティがあり、ここに
  // cdk.CustomResource の properties に指定した値が格納される。
  const { name } = event.ResourceProperties; // ここで properties で指定した name を取り出しています。
}

以上で、カスタムリソースのライフサイクルを管理する Lambda 関数が定義できました。

ここまででカスタムリソースを作成する一連の方法について解説していきました。 詳細なライフサイクル管理について知りたいかたは Provider Framework のドキュメントをご覧ください。 特に、 Update や Delete, Create 時に失敗したときにこちらがどのように対応すべきかについては Important cases to handle にまとまっていますので、一読しておくことをおすすめします。

Outposts IaC with AWS CDK

ここまでで読み進めるにあたって必要な準備が整いましたので、さっそく本題に入ります。 今回は AWS CDK を使って Outposts に展開するリソースを定義する方法について私達が試した範囲で記載していきます。

subnet

subnet 自体をデプロイするのは簡単です。 既に CloudFormation で展開したい Outpost 筐体の ARN を指定すればデプロイできるようになっています。 ただし、ローレベルコンストラクターのみ Outpost ARN の設定に対応しています。 ハイレベルコンストラクターからどの Outpost 筐体へデプロイするかの設定はできませんが、どの Outpost 筐体にデプロイされたのかを subnetOutpostArn から取得することのみはサポートされています7

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

export class Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    // ...

    new ec2.CfnSubnet(this, "Subnet", {
      // ...
      outpostArn: "arn:aws:outposts:<デプロイ先 Outpost 筐体がデプロイされているリージョン>:<デプロイ先 Outpost 筐体を所持するAWS アカウント ID>:outpost/<デプロイ先 Outpost 筐体の Outposts ID>",
      // outpostARN を指定する場合、対象 outpost 筐体が所属している AZ も
      // 指定しておかなければならない
      availabilityZone: "<デプロイ先 Outpost が所属している AZ>",
    });
  }
}

ちなみに、 CloudFormation で subnet へ展開するリソースに public IP を自動で付与する設定8はありますが、 customer owned IP address 9に対応するそのような設定は現在 CloudFormation ではサポートされていません。 これが設定できないと、例えば ALB や NAT をデプロイした際に customer owned IP address を自動でつけてくれないので、オンプレミス側から NAT や ALB を参照できません10。 現状、この設定をするにはカスタムリソースを使って AWS API を呼び出して設定しなければなりませんので、ここでカスタムリソースを使う必要があります。 実際には ModifySubnetAttribute を呼び出せば目的は達成できるため、前述の Lambda 関数中で対応する AWS API を作成/更新時に呼び出せば OK です。 サンプルコードは こちら に用意したので、参考にしてください。

EBS Volume

EC2 インスタンスデプロイ時にくっつくルートボリュームを担う EBS Volume は、 EC2 インスタンスを Outpost 筐体上の subnet に配置することで自動的にその subnet と同じ Outpost 筐体上にデプロイされます。 一方、自分で追加の EBS Volume をつくる場合はローレベルコンストラクターである CfnVolume コンストラクター にデプロイ先 Outpost 筐体の ARN を指定すれば OK です。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

export class Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    // ...

    new ec2.CfnVolume(this, "Volume", {
      // ...
      outpostArn: "arn:aws:outposts:<デプロイ先 Outpost 筐体がデプロイされているリージョン>:<デプロイ先 Outpost 筐体を所持するAWS アカウント ID>:outpost/<デプロイ先 Outpost 筐体の Outposts ID>",
    });
  }
}

ただし、注意点として、 EC2 インスタンスに対応するハイレベルコンストラクターである Instance コンストラクター 内の EBS Volume を定義するためのプロパティである blockDevices では、 Outposts ARN を指定できません11。 したがって Outpost 筐体上に展開する追加の EBS Volume を EC2 インスタンスへアタッチする場合は、

  1. ローレベルである CfnVolume を使って Outpost 筐体上にデプロイするように設定し、 CfnVolumeAttachment でアタッチする
  2. そもそもローレベルコンストラクターでインスタンスも定義する

の2択になります。 次のコードは 1. の選択肢をとった場合の例です。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct} from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

export class Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    // ...

    // インスタンスを用意
    const instance = new ec2.Instance(this, "Instance", { /* ... */ });
    // 追加の EBS Volume を用意
    const additionalVolume = new ec2.CfnVolume(this, "AdditionalVolume", {
      // ...
      outpostArn: "arn:aws:outposts:<デプロイ先 Outpost 筐体がデプロイされているリージョン>:<デプロイ先 Outpost 筐体を所持するAWS アカウント ID>:outpost/<デプロイ先 Outpost 筐体の Outposts ID>",
    });
    //EBS Volume をアタッチ
    new ec2.CfnVolumeAttachment(this, "AdditionalVolumeAttachment", {
      device: '<アタッチ先デバイス名>',
      volumeId: additionalVolume.ref,
      instanceId: instance.instanceId,
    });
  }
}

ALB

ALB の場合は customer owned IP address を付与しない場合はハイレベルコンストラクターでも設置先 subnet を指定できる12ので、 Outposts 筐体上に展開しているサブネットへデプロイすれば Outpost 筐体上へ自動でデプロイされます。

しかし、 customer owned IP address を付与しようとした ALB は現在 CloudFormation できません13。 なので、 customer owned IP address を ALB へ付与する場合はロードバランサーの作成段階からカスタムリソースを使う必要があります14。 実際のコードは リポジトリー を載せたのでそちらを確認してください。 基本的にはロードバランサーを作成/削除してるだけです。

S3 on Outposts

S3 on Outposts はローレベルなもののみ提供されています15。 ちなみに、 CDK で提供されているモジュールはリファレンスを参照するとまだプレビュー段階であることが伺えます。 まだ安定はしてないようなので、利用する際には注意が必要です。

The construct library for this service is in preview. Since it is not stable yet, it is distributed as a separate package so that you can pin its version independently of the rest of the CDK.

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3outposts-readme.html

前回も書きましたが、 S3 on Outposts を利用するためには次の2つを必要とします。

  • Outposts エンドポイント
  • S3 アクセスポイント

これらは VPC 上へ設置することになるため、 VPC からのみ S3 on Outposts バケットへアクセスできます。 したがって、 S3 on Outposts を利用できるようにするにはバケットを用意しつつ、この2つを VPC 上に設置すれば OK です。

S3 アクセスポイントは

  • 接続先バケットの ARN
  • デプロイ先 VPC の VPC ID
  • アクセスポイント名

の設定がデプロイに最低限必要です。 Outposts エンドポイントは

  • デプロイ先 Outpost 筐体についている Outposts ID
  • エンドポイントにつける セキュリティグループ ID
  • デプロイ先サブネットの ID

の3つがデプロイに必要です。 ちなみに、もしデプロイする場合、 CfnEndpoint のドキュメントにもありますが、エンドポイントの作成に時間がかかるので注意してください。

It can take up to 5 minutes for this resource to be created.

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3outposts.CfnEndpoint.html

これらをデプロイするコード例は次のようになります。

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct} from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as s3outposts from 'aws-cdk-lib/aws-s3outposts';

export class Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    // ...

    // S3 アクセスポイントは VPC にデプロイされるので、 S3 アクセスポイントを設置したい VPC を定義
    const vpc = new ec2.Vpc(this, "Vpc", { /* ... */ });
    // Outposts エンドポイントはサブネットへ紐付くため、 Outposts エンドポイントを設置したい サブネットを定義
    const subnet = new ec2.Subnet(this, "Subnet", {
      // ...
      vpcId: vpc.vpcId,
      outpostArn: "arn:aws:outposts:<デプロイ先 Outpost 筐体がデプロイされているリージョン>:<デプロイ先 Outpost 筐体を所持するAWS アカウント ID>:outpost/<デプロイ先 Outpost 筐体の Outposts ID>",
    });

    // S3 on Outposts バケットを用意する
    const bucket = new s3outposts.CfnBucket(this, "Bucket", {
      bucketName: "<デプロイしたいバケット名>",
      outpostId: "<デプロイ先 Outpost 筐体の Outposts ID>",
    });
    // S3 アクセスポイントを用意する
    new s3outposts.CfnAccessPoint(this, "AccessPoint", {
      bucket: bucket.ref, // このアクセスポイントで接続する S3 on Outposts のバケット ARN を指定する。ここでは先にバケットを定義しており、そこからできる ARN を参照するようにする。
      // デプロイ先の VPC の設定をする
      vpcConfiguration: {
        vpcId: vpc.vpcId, // 現状、デプロイ先 VPC の ID しか設定するプロパティが用意されてない
      },
      name: "<アクセスポイントに設定したいアクセスポイント名>",
    });
    // Outposts エンドポイントは Security Group を要求してくるので用意する
    const endpointSecurityGroup = new ec2.SecurityGroup(this, "EndpointSecurityGroup", { /* ... */ });
    // Outposts エンドポイントを用意する
    new s3outposts.CfnEndpoint(this, "Endpoint", {
      outpostId: "<デプロイ先 Outpost 筐体の Outposts ID>",
      securityGroupId: endpointSecurityGroup.securityGroupId,
      subnetId: subnet.ref,
    });
  }
}

まとめ

今回は AWS CDK で Outposts を IaC する方法の紹介をしてきました。 一部サービスはまだ多少こちらでカスタムリソースを使って管理しなければならないことが見てとれると思います。 カスタムリソースを利用するには多少 AWS API を利用して AWS リソースを作成することに習熟しておく必要があり、また AWS CDK でカスタムリソースを用意する知識が必要なため、ハードルは高めです。 そのため、現状では AWS CDK で Outposts をやるのはまだ難しいところがあります。 しかし、少しずつ改善はされ、以前はできなかったことができるようになってきたりなどしているので、今後も状況の改善がなされていくことが期待できます。


  1. https://aws.amazon.com/jp/cdk/

  2. これ以外にも patterns というものがあります。こちらはハイレベルコンストラクターのものをまとめて、一般的に利用されるシーンのものに合わせてさらに抽象度を高めています。例としては定期的に実行する Fargate Task を定義する ScheduledFargateTask という pattern が提供されています。

  3. AWS の新サービスだとローレベルコンストラクターが提供されているがハイレベルコンストラクターは提供されてないという場合があったり、ハイレベルなものが提供されているものの、設定したいものがハイレベルだと設定できないということがあります。そういう場合にローレベルコンストラクターを使います(実はハイレベルコンストラクターのプロパティには node というプロパティが生えており、そこから実際にバックで利用しているローレベルコンストラクターにアクセスできるのでそちらから一部設定値をいじることができたりします)。

  4. ここでは @types/aws-lambda パッケージを利用して型付けしています

  5. CDK だと aws-cdk-lib/custom-resourcesCustomResource コンポーネントに生えてる getAtt メソッドが対応する関数になります。

  6. 他にも数値型として参照できたりします。詳しくは ドキュメント をご覧ください。

  7. https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Subnet.html#subnetoutpostarn

  8. AWS::EC2::SubnetMapPublicIpOnLaunch

  9. customer owned IP address については前回の記事をご覧ください。

  10. 後で設定はできるので、デプロイ後に自分で設定するといったことはできるのですが、ここではこれも自動管理する話をしています。

  11. blockDevicesの型 (aws-cdk-lib/aws-ec2 の BlockDevice インターフェース) を辿っても Outpost ARN を指定するためのプロパティが表れないため、指定できないということがわかります。

  12. https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationLoadBalancer.html#vpcsubnets

  13. https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-loadbalancer.html

  14. subnet と同じように ModifyLoadBalancerAttributes で customer owned IP address を設定できるか確認したのですが、現状では設定できないようなので、そもそも作成段階からカスタムリソースで管理する必要があります。

  15. https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3outposts-readme.html

© NTT Communications Corporation All Rights Reserved.