MinecraftサーバをAWSで立ててみた #2

# はじめに
この記事は [MinecraftサーバをAWSで立ててみた](https://qiita.com/tonomin/items/de6eb87857fb7abc1d24) の第2回の記事となります。

今回は前回作成したインフラ環境のIaC化を実施してみます。
AWSのインフラ環境のIaCといえば **CDK** ですね。
ということでCDKの作成をやってみましたのでまとめておきます。

今回は、CDK全般のまとめでそこそこの量になったので、Minecraftサーバのためのサブネットやリソースと詳細まとめは次回にします。

# 目次
<!-- タイトルとアンカー名を編集 -->
1. [モチベーション](#Chapter1)
1. [CDK作成](#Chapter2)
1. [参考文献](#reference)

<!-- 各チャプター -->
<a id="#Chapter1"></a>
# モチベーション
前回が事実上Minecraftサーバの動作確認だけだったのに、早速CDKを作っていく理由的なものを残しておこうと思います。

これから個人でサーバ構築・運用を試していく上で何より重要視していることは

**「いかにお金を掛けないか」**

です。

サービスが止ろうが、サーバがぶっ壊れようが大した問題ではありません。
AWS上のリソースを利用することでかさんでいくランニングコストを如何に抑えつつ、様々なことを試していくかに重きを置きました。

検証を行わないときはインスタンスの停止ではなく**削除**をすることでコスト削減できます。
またテスト的にサーバの設定を変更し、壊れてしまっても一度環境を捨てて、すぐに動作する環境を作成できることは、精神衛生上も好ましいです。

<a id="#Chapter2"></a>
# CDK作成
CDKを書いてみるにあたってClassmethodさんの記事をめちゃくちゃ参考にさせてもらいました。
[実践！AWS CDK](https://dev.classmethod.jp/search/?q=%E5%AE%9F%E8%B7%B5%EF%BD%80CDK&page=1) の記事シリーズは本当に最高です！
今回作成したCDKのソースはこの記事をベースにしており、サーバ構成やNatGatewayの部分を主に変更して作成しました。なのでソースの細かな部分は上記の記事を参照いただければと思います。

自分が変更した部分をピックアップしてまとめます。
（一応、CDKで実装した構成図を再掲します）

![minecraft_server.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1340134/876e0165-0a40-553a-360e-329655c41e29.jpeg)


### 実行環境・言語
[実践！AWS CDK](https://dev.classmethod.jp/search/?q=%E5%AE%9F%E8%B7%B5%EF%BD%80CDK&page=1) では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](https://dev.classmethod.jp/search/?q=%E5%AE%9F%E8%B7%B5%EF%BD%80CDK&page=1) で紹介されていたものを利用していますが、簡単に紹介します。

```
.
├── 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 リファクタリング](https://dev.classmethod.jp/articles/cdk-practice-9-refactoring/)でも紹介されていますが、リソース定義する際に同じクラスのインスタンス化をベタ書きすることを避け、リソースのパラメータがわかりやすい形でまとめるようにしました。

>- 作成するリソースごとにファイル（クラス）を分割する
>   - 例）internetGateway.ts（InternetGateway クラス）
>- すべてのリソースクラスは抽象クラス Resource を継承する
>- 「あるリソースの生成に必要となるリソース」はコンストラクタに渡す
>   - 渡すオブジェクトは CfnXXX クラスのインスタンス
>- リソースクラス内で複数のリソースを生成する場合はループで回す
>   - new CfnXXX() を何度も実行しない
>   - ResourceInfo インタフェースを用意してプロパティの変動部分のみを書き出す
>- 生成したリソースはリソースクラス内の public メンバ変数に格納し、外部クラスから参照可能とする

上記ルールを`Python`でも実現できるようにコードを修正しています。
```Python:resources/vpc.py
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](https://dev.classmethod.jp/search/?q=%E5%AE%9F%E8%B7%B5%EF%BD%80CDK&page=1) ではNatGatewayを各AZに１台作成しています。

**NatGatewayは高いです**

NatGatewayは稼働時間料金で33.48USD/月、これに加えてデータ処理および転送料金が発生します。一方でNatInstanceはt3.microのオンデマンドで10.12USD/月程度です。
検証目的であればNatInstanceで十分で、費用も抑えれます。

今回は2AZの構成ですが、NatInstanceは１台のみで運用します。

NatInstanceをCDKで作成する場合、`L2: High-level constructs`の`VPC`クラスでリソースを作成すれば簡単に作成できます。しかし、`L2: High-level constructs`のクラスを使用すると、こちらで明示的に定義していないリソースも勝手に作成されてしまいます。（良くも悪くも、AWSのおすすめの構成が自動的に作られます）
CDKにおけるレイヤーの理解は重要なので[実践！AWS CDK #1 導入](https://dev.classmethod.jp/articles/cdk-practice-1-introduction/)を一読することをおすすめします。

ということで、１からNatInstanceを定義することになるのですが、リソース定義を`resources/nat_instance.py`に実装しました。NatInstanceは実際ただのEC2インスタンスなのですが、役割としては`VpcStack`に所属すべきだと考え、`VpcStack`のリソースとしてインスタンス化しています。

ただ、`VpcStack`に所属させるために、一つのファイルに、NatInstanceの定義と、NatInstanceのためのSecurityGroupの定義が混じってしまっています。
ここは改善したいポイントです。

```Python:resources/nat_instance.py
# (略)

# ここから 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で公開](https://github.com/tonomin/minecraft_server_cdk)しています。
クレデンシャル等は入っていないと思うのですが、もしこのファイルは公開しないほうが良いよといったことがあれば是非コメントでご指摘ください。

<a id="#reference"></a>
# 参考文献
- [実践！AWS CDK](https://dev.classmethod.jp/search/?q=%E5%AE%9F%E8%B7%B5%EF%BD%80CDK&page=1)