[4章] CDK for Terraformで、AWS Fargateの環境構築 [前編]

takahiko tominaga
nextbeat-engineering
24 min readSep 8, 2021

目次

  1. はじめに
  2. 概要
  3. CDK for Terraformの初期化
  4. VPCの構築
  5. サブネットの構築
  6. インターネットゲートウェイの構築
  7. NATゲートウェイの構築
  8. ルートテーブルの構築
  9. セキュリティグループの構築
  10. CDK for TerraformのDeploy・Destroy
  11. おわりに

1. はじめに

こんにちは、ネクストビートの富永です。
本ブログでは、CDK for Terraformを使用して、AWSの環境構築を行います。

今回は環境構築を行なっていきますが、環境自体の説明というよりもTerraformとCDK for Terraformでの書き方の違いなどを説明できればと思います。

環境構築自体は、ネットワークレイヤー(セキュリティーレイヤーも含む)の構築を行なっていき、次回のブログでECS等のアプリケーションレイヤーの構築を行なっていく予定です。

本ブログは、複数の章で分けられた一部であり、その4章目となります。
前回までのブログは、以下に記載しておきます。
今回は、AWSの環境構築だけですが、前回までのブログの内容も関わって来ますのでまだ読んでいない方は、前回までの章を先に読むことをおすすめします。

2. 概要

今回は、AWSのネットワークレイヤーとセキュリティーレイヤーに関わる部分の構築を行って行きます。
具体的には、以下項目の構築を行います。

  • VPC
  • サブネット
  • インターネットゲートウェイ
  • NATゲートウェイ
  • ルートテーブル
  • セキュリティグループ

構成図

3. CDK for Terraformの初期化

まずはCDK for Terraformを使用できるように設定を行います。
設定は、下記公式を参照しています。

CDK for Terraform: Enabling Python & TypeScript Support

3. 1 Terraformのインストール

Terraformをインストールしていないと、下記のようなエラーが発生してしまいます。

# Terraform CLI not present - Please install a current version https://learn.hashicorp.com/terraform/getting-started/install.html

なのでまずは、Terraformをインストールしていきます。

公式: Install Terraform

$ brew tap hashicorp/tap
$ brew install hashicorp/tap/terraform

インストール完了後、Terraformはバージョンアップに追従しやすいtfenvの利用を推奨しているので、以下手順でtfenvもインストールします。

  1. Terraformのバージョンマネージャである、tfenvをインストール
  2. 現在使用可能なTerraformのバージョンを確認
  3. 使用したいTerraformのバージョンをインストールする
  4. 使用したいTerraformのバージョンを使用する
$ brew install tfenv
$ tfenv list-remote
$ tfenv install 使用したいバージョン
$ tfenv use 使用したいバージョン

インストール完了後、以下コマンドでバージョンを確認できれば完了です。

$ terraform --version
Terraform v0.14.8
$ tfenv --version
tfenv 2.0.0

3.2 CDK for Terraformのインストール

Terraformのインストールが終わったので、本命のCDK for Terraformをインストールしていきます。
今回は、公式と同じように下記コマンドで、グローバルインストールを行います。

$ npm install -g cdktf-cli

グローバルインストールが嫌な場合は、以下記事を参考にインストールと初期化を行ってください。

AWS CDKでプロバイダーとしてTerraformが使える!!CDK for Terraformが発表されました!!

3.3 CDK for Terraformの初期化

以下コマンドを実行し、新規プロジェクトの初期化を行います。
今回は、TypeScriptを使用しますので、templateでtypescriptを指定します。

$ mkdir sample-cfktf
$ cd sample-cfktf
$ cdktf init --template=typescript

初期化完了後、ファイルが生成されていれば成功です。

初期化完了後のファイル一覧に.genディレクトリがなければ、以下手順を行う必要があります。

作成したプロジェクトで、再度 npm install cdktf-cliを行うもしくは、生成されたcdktf.jsonを以下のように編集します。

{
"language": "typescript",
"app": "npm run --silent compile && node main.js",
"terraformProviders": [
"aws@~> 2.0" // <- 追加
],
...
}

上記のように、terraformProvidersの中にaws@~があれば、次は以下コマンドを実行します。

$ cdktf get

実行後、Generated typescript constructs in the output directory: .genと表示されてプロジェクトにも.genディレクトリ生成されていれば、成功です。

CDK for Terraformは、この.genディレクトリにパッケージを置いておくようです。
今回は、awsのみインストールしています。
これで、aws用のパッケージをimportして使用することができるようになりました。

ここまでで、CDK for Terraformの初期化は完了です。
次からは、ようやくAWSの環境構築を行っていきます。

4. VPCの構築

まずは、VPCの構築を行います。
CDK for Terraformは、main.tsというファイルに設定を記載していきます。
(以下は、プロダクト初期化時に生成されているコードです)

import { Construct } from 'constructs';
import { App, TerraformStack } from 'cdktf';

class MyStack extends TerraformStack {
constructor(scope: Construct, name: string) {
super(scope, name);

// define resources here
}
}

const app = new App();
new MyStack(app, 'sample-cdktf');
app.synth();

以下のように.genに置いてあるprovidersからawsを指定して、AwsProviderをインポートします。

import { AwsProvider } from './.gen/providers/aws';

インポートしたらAwsProviderを使用して、プロバイダーを設定します。

new AwsProvider(this, 'sample-cdktf', {
region: 'ap-northeast-1'
});

次は、同じようにVpcをインポートします。

import { ..., Vpc } from './.gen/providers/aws';

インポート後、以下のようにVPCの設定を追記します。
VPCは、サブネット作成時に使用するので、変数化しておきます。

Terraformだと特に変数化をしなくても、aws_vpc.xxxx.idのようにして実装したリソースの値を使用できました。

ただ CDK for Terraformだと任意の言語で書ける反面、このように変数化をしておかないと実装したリソースの値を使用できません。

const vpc = new Vpc(this, 'sample-cdktf-vpc', {
cidrBlock: '10.0.0.0/16',
enableDnsHostnames: true,
tags: { ['Name']: 'sample-cdktf production' }
});

追記したら、VPCの設定は完了です。
今回は、全ての設定完了後デプロイを行って確認を行います。
都度確認したい方は、以下コマンドで実装内容を確認することができます。

ここら辺は、Terraformのplanコマンドと同じですね。

$ cdktf plan

5. サブネットの構築

次は、サブネットの構築を行います。
今回は、PublicとPrivateを2つずつの計4つ作成を行います。

VPCと同じように、まずはSubnetをインポートします。

import { ..., Subnet } from './.gen/providers/aws';

インポート後、以下のようにサブネットの設定を追記します。

const publicSubnet1  = new Subnet(this, 'sample-cdktf-public-subnet1', {
vpcId: Token.asString(vpc.id),
availabilityZone: 'ap-northeast-1a',
cidrBlock: '10.0.0.0/20',
mapPublicIpOnLaunch: true,
tags: { ['Name']: 'Public1 AZ1a sample-cdktf' }
});

VPCのIdをToken.asStringを使用して、設定をしていますが理由は、以下公式と他の人のブログで記載されていました。
公式

To create a subnet based on the VPC identifier, pass the vpc.id to a Subnet object. The main.ts demonstrates the creation of the VPC and a subnet based on its identifier. The example uses a Token to cast the VPC ID as a string type and enables the CDK for Terraform to resolve the value of the VPC ID later in synthesis. [1]

他の方のブログ

CDKでは作成するリソースのIDなど作成するまで未確定のものはTokenとして扱います。Tokenとして扱われていたとしても、他のリソースで参照する際は自動で実行時にTokenが解除されました。なので、 console.log(vpc.id) のような事をしなければTokenとして扱われている事に気づかない程でした。
しかし、CDK for Terraformでは、Tokenを参照する場合 Token.asString(vpc.id) の様にする必要がありました。[2]

残り3つのサブネットも同じように作成を行います。
※以下全てのサブネットを記載しています。

/** Public Subnet */
const publicSubnet1 = new Subnet(this, 'sample-cdktf-public-subnet1', {
vpcId: Token.asString(vpc.id),
availabilityZone: 'ap-northeast-1a',
cidrBlock: '10.0.0.0/20',
mapPublicIpOnLaunch: true,
tags: { ['Name']: 'Public1 AZ1a sample-cdktf' }
});

const publicSubnet2 = new Subnet(this, 'sample-cdktf-public-subnet2', {
vpcId: Token.asString(vpc.id),
availabilityZone: 'ap-northeast-1c',
cidrBlock: '10.0.16.0/20',
mapPublicIpOnLaunch: true,
tags: { ['Name']: 'Public2 AZ1c sample-cdktf' }
});
/** Private Subnet */
const privateSubnet1 = new Subnet(this, 'sample-cdktf-private-subnet1', {
vpcId: Token.asString(vpc.id),
availabilityZone: 'ap-northeast-1a',
cidrBlock: '10.0.128.0/20',
mapPublicIpOnLaunch: true,
tags: { ['Name']: 'Private1 AZ1a sample-cdktf' }
});

const privateSubnet2 = new Subnet(this, 'sample-cdktf-private-subnet2', {
vpcId: Token.asString(vpc.id),
availabilityZone: 'ap-northeast-1c',
cidrBlock: '10.0.144.0/20',
mapPublicIpOnLaunch: true,
tags: { ['Name']: 'Private2 AZ1c sample-cdktf' }
});

全て追記し終えたらサブネットの構築は完了です。

6. インターネットゲートウェイの構築

次は、インターネットゲートウェイの構築を行います。

他と同じように、まずはInternetGatewayをインポートします。

import { ..., InternetGateway } from './.gen/providers/aws';

インポート後、以下のようにインターネットゲートウェイの設定を追記します。

const internetGateway = new InternetGateway(this, 'sample-cdktf-igw-production', {
vpcId: Token.asString(vpc.id),
tags: { ['Name']: 'igw-production sample-cdktf' }
});

これで、インターネットゲートウェイの構築は完了です。

7. NATゲートウェイの構築

次は、NATゲートウェイの構築を行います。

他と同じように、まずはNatGatewayとEipをインポートします。

import { ..., NatGateway, Eip } from './.gen/providers/aws';

インポート後、まずは以下のようにEipの設定を追記します。

const eip = new Eip(this, 'sample-cdktf-eip', {
vpc: true
})

次は、NatGatewayの設定を追記します。

const natGateway = new NatGateway(this, 'sample-cdktf-nat-gateway', {
allocationId: Token.asString(eip.id),
subnetId: Token.asString(publicSubnet1.id),
tags: { ['Name']: 'sample-cdktf nat-gateway-production' }
});

これで、NATゲートウェイの構築は完了です。

8. ルートテーブルの構築

次は、ルートテーブルの構築を行います。
今回は、PublicとPrivate用に2つ作成します。

他と同じように、まずはRouteTableをインポートします。

import { ..., RouteTable } from './.gen/providers/aws';

インポート後、以下のようにRouteTableの設定を追記します。

const publicRouteTable = new RouteTable(scope, 'sample-cdktf-public-rtb', {
vpcId: Token.asString(vpc.id),
route: [{
cidrBlock: '0.0.0.0/0',
gatewayId: Token.asString(internetGateway.id),
ipv6CidrBlock: '',
egressOnlyGatewayId: '',
instanceId: '',
natGatewayId: '',
networkInterfaceId: '',
transitGatewayId: '',
vpcPeeringConnectionId: ''
}],
tags: { ['Name']: 'sample-cdktf Public rtb' }
});

ルートテーブルは、上記のように設定しない項目も空文字で設定しておかないとエラーになってしまいます。
以下Issuesでバグとして報告されていますので、後々修正されると思います。
https://github.com/hashicorp/terraform-provider-aws/issues/13854

Private用のルートテーブルも同様に追記します。

const privateRouteTable = new RouteTable(scope, 'sample-cdktf-private-rtb', {
vpcId: Token.asString(vpc.id),
route: [{
cidrBlock: '0.0.0.0/0',
gatewayId: '',
ipv6CidrBlock: '',
egressOnlyGatewayId: '',
instanceId: '',
natGatewayId: Token.asString(natGateway.id),
networkInterfaceId: '',
transitGatewayId: '',
vpcPeeringConnectionId: ''
}],
tags: { ['Name']: 'sample-cdktf Private rtb' }
});

ルートテーブルの設定を追記したら、次はルートテーブルとサブネットの紐付けを行います。

まずは、以下のようにRouteTableAssociationをインポートします。

import { ..., RouteTableAssociation } from './.gen/providers/aws';

インポート後、以下のようにRouteTableとSubnetの紐付けを行います。

new RouteTableAssociation(this, 'sample-cdktf-public-rtb1', {
routeTableId: Token.asString(publicRouteTable.id),
subnetId: Token.asString(publicSubnet1.id)
});

残りのサブネットも同様に紐付けを行います。
※以下全ての紐付けを記載しています。

/** Association to Public RouteTable */
new RouteTableAssociation(this, 'sample-cdktf-public-rtb-association1', {
routeTableId: Token.asString(publicRouteTable.id),
subnetId: Token.asString(publicSubnet1.id)
});
new RouteTableAssociation(this, 'sample-cdktf-public-rtb-association2', {
routeTableId: Token.asString(publicRouteTable.id),
subnetId: Token.asString(publicSubnet2.id)
});

/** Association to Private RouteTable */
new RouteTableAssociation(this, 'sample-cdktf-private-rtb-association1', {
routeTableId: Token.asString(privateRouteTable.id),
subnetId: Token.asString(privateSubnet1.id)
});
new RouteTableAssociation(this, 'sample-cdktf-private-rtb-association2', {
routeTableId: Token.asString(privateRouteTable.id),
subnetId: Token.asString(privateSubnet2.id)
});

これで、ルートテーブルの構築は完了です。

9. セキュリティグループの構築

次は、セキュリティグループの構築を行います。

他と同じように、まずはSecurityGroupをインポートします。

import { ..., SecurityGroup } from './.gen/providers/aws';

インポート後、以下のようにSecurityGroupの設定を追記します。

const security = new SecurityGroup(this, 'sample-cdktf-security-group', {
name: 'sample-cdktf',
vpcId: Token.asString(vpc.id),
tags: { ['Name']: 'sample-cdktf' }
});

次は、ルールの設定を行います。
以下のように、SecurityGroupRuleをインポートします。

import { ..., SecurityGroupRule } from './.gen/providers/aws';

インポート後、以下のようにSecurityGroupRuleの設定を追記します。
インバウンドルールとアウトバウンドルールの設定をしています。

new SecurityGroupRule(this, 'sample-cdktf-security-ingress', {
cidrBlocks: ['0.0.0.0/0'],
fromPort: 80,
protocol: 'tcp',
securityGroupId: Token.asString(security.id),
toPort: 80,
type: 'ingress'
});
new SecurityGroupRule(scope, 'sample-cdktf-security-egress', {
cidrBlocks: ['0.0.0.0/0'],
fromPort: 0,
protocol: 'all',
securityGroupId: Token.asString(security.id),
toPort: 0,
type: 'egress'
});

これで、セキュリティグループの構築は完了です。

10. CDK for TerraformのDeploy・Destroy

10.1 Deploy

設定したものを以下コマンドで、AWSにデプロイしてみます。

$ cdktf deploy

コマンド実行後、以下のようにデプロイを行う一覧が表示されます。

「yes」と入力してEnterを押します。
するとデプロイ作業が始まりますので、完了するまでしばらく待ちます。

デプロイが完了したら、AWSのマネジメントコンソールから設定した物が、生成されているかを確認します。

VPC

Subnet

InternetGateway

NatGateway

Route Table

SecurityGroup

10.2 Destroy

全て確認できたら、最後にデプロイされたものを、以下コマンドで削除しておきます。

$ cdktf destroy

デプロイ時と同様に、削除する項目の一覧が表示されますので、「yes」と入力してEnterを押します。

デプロイと同じように、削除されたか確認しておきましょう。

無事削除されていれば、今回行う設定は全て完了です。

11. おわりに

今回も長くなりましたが、最後まで読んでいただき、ありがとうございます。

CDK for Terraformは、Terraformと違ってモジュール分割がとてもやりにくいなと感じました。
別クラスを作成して設定すればファイル分割はできますが、これじゃない感がすごいので、Terraformのようにモジュール分割できるようになればなと思いました。

また全ての実装にStackを渡す必要があり(実装のthisの部分)ファイル分割を行なっても、main.ts(TerraformStackを継承しているクラス)ファイル内に集約しなければいけないのが、環境が大きくなればなるほどファイル行も大きくなり見にくくなってしまうので、微妙だと感じました。
(もっといい方法があるのかもしれませんが。。。)

ただ、Terraformを触ったことがない人が最初に触ってみる分には、慣れ親しんだ言語で実装を行えるのでいいかなと思いました。

次の後編では、アプリケーションレイヤーの構築を行っていきます。
後編も長くなりますが、最後まで読んでいただけると嬉しいです。
それでは、次の後編でお会いしましょう。

参考文献

[1] CDK for Terraform: Enabling Python & TypeScript Support
[2] AWS CDKでプロバイダーとしてTerraformが使える!!CDK for Terraformが発表されました!!
[3] terraform-cdk で AWSにリソースをdeployする

We are hiring!

株式会社ネクストビートでは

「人口減少社会において必要とされるインターネット事業を創造し、ニッポンを元気にする。」
を理念に掲げ一緒に働く仲間を募集しております。

https://www.nextbeat.co.jp/recruit

--

--