はじめに
この記事は MinecraftサーバをAWSで立ててみた の第2回の記事となります。
今回は前回作成したインフラ環境のIaC化を実施してみます。
AWSのインフラ環境のIaCといえば CDK ですね。
ということでCDKの作成をやってみましたのでまとめておきます。
今回は、CDK全般のまとめでそこそこの量になったので、Minecraftサーバのためのサブネットやリソースと詳細まとめは次回にします。
目次
モチベーション
前回が事実上Minecraftサーバの動作確認だけだったのに、早速CDKを作っていく理由的なものを残しておこうと思います。
これから個人でサーバ構築・運用を試していく上で何より重要視していることは
「いかにお金を掛けないか」
です。
サービスが止ろうが、サーバがぶっ壊れようが大した問題ではありません。
AWS上のリソースを利用することでかさんでいくランニングコストを如何に抑えつつ、様々なことを試していくかに重きを置きました。
検証を行わないときはインスタンスの停止ではなく削除をすることでコスト削減できます。
またテスト的にサーバの設定を変更し、壊れてしまっても一度環境を捨てて、すぐに動作する環境を作成できることは、精神衛生上も好ましいです。
CDK作成
CDKを書いてみるにあたってClassmethodさんの記事をめちゃくちゃ参考にさせてもらいました。
実践!AWS CDK の記事シリーズは本当に最高です!
今回作成したCDKのソースはこの記事をベースにしており、サーバ構成やNatGatewayの部分を主に変更して作成しました。なのでソースの細かな部分は上記の記事を参照いただければと思います。
自分が変更した部分をピックアップしてまとめます。
(一応、CDKで実装した構成図を再掲します)
実行環境・言語
実践!AWS CDK ではCDKの実装言語にTypescript
を選定されています。自分はTypeScript
には明るくないのでPython
で書くことにしました。
また、CDKではデフォルトでvirtualenv
でのcdk
コマンドの実行するようなチュートリアルとなっていますが、Pipenv
のほうが使いやすいので、こちらを使用するように変更しました。
よって実行環境はざっくり以下となりました。
環境 | バージョン |
---|---|
aws cli | 2.3.7 |
aws cdk | 2.10.0 |
python | 3.9.12 |
Pipenv | - |
ディレクトリ構成
ディレクトリ構成は実践!AWS CDK で紹介されていたものを利用していますが、簡単に紹介します。
.
├── Pipfile
├── Pipfile.lock
├── README.md
├── app.py # mainのソースファイル
├── cdk.json
├── cdk.out/ # Cfnのファイル出力先
├── docs/ # README.md関連のドキュメント格納用
│
├── minecraft_server/
│ ├── resources/ # リソースごとの定義ファイルを格納
│ │ ├── abstract/
│ │ │ └── resource.py
│ │ ├── ec2.py
│ │ ├── ...
│ └── stacks/ # スタックごとの定義ファイルを格納
│ ├── ec2_stack.py
│ ├── ...
├── requirements-dev.txt
├── requirements.txt
├── scripts/ # 各EC2作成時に実行するuserData.shを格納
└── tests/ # cdkのテストコード格納先(テストコードは未作成)
リソース定義の実装ルール
実践!AWS CDK #9 リファクタリングでも紹介されていますが、リソース定義する際に同じクラスのインスタンス化をベタ書きすることを避け、リソースのパラメータがわかりやすい形でまとめるようにしました。
- 作成するリソースごとにファイル(クラス)を分割する
- 例)internetGateway.ts(InternetGateway クラス)
- すべてのリソースクラスは抽象クラス Resource を継承する
- 「あるリソースの生成に必要となるリソース」はコンストラクタに渡す
- 渡すオブジェクトは CfnXXX クラスのインスタンス
- リソースクラス内で複数のリソースを生成する場合はループで回す
- new CfnXXX() を何度も実行しない
- ResourceInfo インタフェースを用意してプロパティの変動部分のみを書き出す
- 生成したリソースはリソースクラス内の public メンバ変数に格納し、外部クラスから参照可能とする
上記ルールをPython
でも実現できるようにコードを修正しています。
from .abstract import Resource
from dataclasses import dataclass
# vpcでは出現しないが、リソースのパラメータが値がクラスとなっているような場合(クラスのネスト)
# 普通のdataclassではdict->dataclassの変換ができない
# 上記を達成するために validated_dcを pip installし、ResourceInfoで継承する
from validated_dc import ValidatedDC
from aws_cdk import (
aws_ec2 as ec2,
)
from constructs import Construct
# リソースのパラメータはdataclassに格納できるようにResourceInfoクラスを定義
@dataclass
class ResourceInfo(ValidatedDC):
id: str
cidr_block: str
resource_name: str
_assign_name: str
class Vpc(Resource):
def __init__(self) -> None:
# リソースのパラメータを定義
self._resources = [
{
'id': 'Vpc',
'cidr_block': '10.8.0.0/16',
'resource_name': 'Vpc',
'_assign_name': 'vpc',
}
]
# パラメータのdictをdataclassに変換
self.resources = [ResourceInfo(**resource) for resource in self._resources]
def create_resources(self, scope: Construct):
for resource_info in self.resources:
vpc = self.__create_vpc(scope, resource_info)
# 実践!AWS CDK では、パラメータのオブジェクトに無名関数を定義して
# 自分自身のpublicメンバにリソースをsetしている
# 同様な形でPythonでも再現できそうだが、一旦'_assign_name'をメンバ変数名として
# setattrする形で実現した
setattr(self, resource_info._assign_name, vpc)
def __create_vpc(self, scope: ec2.CfnVPC, resource_info: ResourceInfo) -> ec2.CfnVPC:
return ec2.CfnVPC(scope,
resource_info.id,
cidr_block=resource_info.cidr_block,
tags=[{'key':'Name', 'value':self._create_resource_name(scope, resource_info.resource_name)}],
)
Nat Instance
実践!AWS CDK ではNatGatewayを各AZに1台作成しています。
NatGatewayは高いです
NatGatewayは稼働時間料金で33.48USD/月、これに加えてデータ処理および転送料金が発生します。一方でNatInstanceはt3.microのオンデマンドで10.12USD/月程度です。
検証目的であればNatInstanceで十分で、費用も抑えれます。
今回は2AZの構成ですが、NatInstanceは1台のみで運用します。
NatInstanceをCDKで作成する場合、L2: High-level constructs
のVPC
クラスでリソースを作成すれば簡単に作成できます。しかし、L2: High-level constructs
のクラスを使用すると、こちらで明示的に定義していないリソースも勝手に作成されてしまいます。(良くも悪くも、AWSのおすすめの構成が自動的に作られます)
CDKにおけるレイヤーの理解は重要なので実践!AWS CDK #1 導入を一読することをおすすめします。
ということで、1からNatInstanceを定義することになるのですが、リソース定義をresources/nat_instance.py
に実装しました。NatInstanceは実際ただのEC2インスタンスなのですが、役割としてはVpcStack
に所属すべきだと考え、VpcStack
のリソースとしてインスタンス化しています。
ただ、VpcStack
に所属させるために、一つのファイルに、NatInstanceの定義と、NatInstanceのためのSecurityGroupの定義が混じってしまっています。
ここは改善したいポイントです。
# (略)
# ここから NatInstance の定義
@dataclass
class ResourceInfo(ValidatedDC):
id: str
availability_zone: str
iam_instance_profile: str
resource_name: str
image_id: str
instance_type: str
source_dest_check: bool
subnet_id: str
eip_id: str
_assign_name: str
class NatInstance(Resource):
def __init__(self,
vpc: ec2.CfnVPC,
iam_role: IAMRole,
subnet_public_1a: ec2.CfnSubnet,
elastic_ip: ec2.CfnEIP) -> None:
self._vpc = vpc
self._resources = [
{
'id': 'Ec2InstanceNat',
'availability_zone': 'us-east-1a',
'iam_instance_profile': iam_role.instance_profile_general_ec2.ref,
'resource_name': 'EC2-NatIns-1a',
# Nat Instance
'image_id': 'ami-0cc6fa590dc4d36eb',
'instance_type': 't2.micro',
'source_dest_check': False,
'subnet_id': subnet_public_1a.ref,
'eip_id': elastic_ip.ref,
'_assign_name': 'natins_1a'
},
]
self.resources = [ResourceInfo(**resource) for resource in self._resources]
def create_resources(self, scope: Construct):
security_group = SecurityGroup(self._vpc)
security_group.create_resources(scope)
# (略)
# ここから SecurityGroup の定義
@dataclass
class SecurityGroupIngressInfo(ValidatedDC):
ip_protocol: str
cidr_ip: str
from_port: int
to_port: int
@dataclass
class IngressInfo(ValidatedDC):
id: str
security_group_ingress_info: SecurityGroupIngressInfo
@dataclass
class NSResourceInfo(ValidatedDC):
id: str
group_description: str
ingresses: List[IngressInfo]
resource_name: str
_assign_name: str
class SecurityGroup(Resource):
def __init__(self, vpc: ec2.CfnVPC) -> None:
self._vpc = vpc
self._resources = [
{
'id': 'SecurityGroupNatInstance',
'group_description': 'For NatInstance',
'ingresses': [
{
'id': 'SecurityGroupIngressNatInstance1',
'security_group_ingress_info': {
'ip_protocol': 'tcp',
'cidr_ip': '10.8.0.0/16',
'from_port': 80,
'to_port': 80,
},
},
{
'id': 'SecurityGroupIngressNatInstance2',
'security_group_ingress_info': {
'ip_protocol': 'tcp',
'cidr_ip': '10.8.0.0/16',
'from_port': 443,
'to_port': 443,
},
},
],
'resource_name': 'Sg-NatIns',
'_assign_name': 'natins'
},
]
self.resources = [NSResourceInfo(**resource) for resource in self._resources]
def create_resources(self, scope: Construct):
for resource_info in self.resources:
# (略)
作成したCDK
完成したソースをgithubで公開しています。
クレデンシャル等は入っていないと思うのですが、もしこのファイルは公開しないほうが良いよといったことがあれば是非コメントでご指摘ください。