EC2, RDS, ALBを用いて、マルチAZ構造を作る。AWSで典型的なVPCを構築して、ウェブアプリを立ち上げる。


<h2>注意</h2>
<p>練習のためにウェブサイトを構築する場合、使い終わったら、EC2インスタンスなどは削除しておきましょう。</p>
<h2>環境</h2>
<ul>
<li>EC2, RDS, ALB, Multi AZ</li>
<li>ubuntu 22.04</li>
<li>Flask 2.1.2</li>
<li>Docker-compose v2.22.0</li>
</ul>
<h2>背景</h2>
<p>典型的なウェブアプリのVPCを構築する練習をしたい。</p>
<h2>結論</h2>
<p>図のようなアーキテクチャを構築する。Multi AZ構成にすることで、可用性を高めることができる。</p>

![7.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508161/b3ff4457-1f97-cb4c-dec7-d94aa8e160ce.png)


<h2>EC2, RDS, ALBを使って動的ウェブアプリをホストする</h2>
<h3>今回のウェブアプリの概要</h3>
<p>初回アクセスの時に、web（Flask）コンテナが、DBにアクセスしてテーブルを作成します。</p>
<p>ウェブサイトへのアクセスがあったら、DBから適当なデータを取得して、それをブラウザに表示する。</p>
<p>docker-composeを用いることで、EC2インスタンスにnginxコンテナとwebコンテナを構築して、webコンテナからRDS（DB）にアクセスする。</p>

<h3>注意</h3>
<p>以下で説明する手順について、AWSコンソールからGUIの操作で行う部分については、細かい説明は省略してあります。</p>

<h3>手順（GUIでアーキテクチャを構築）</h3>
<p>東京リージョンで全ての作業を行うので、東京リージョンを選択してください。</p>
<h4>(1) VPCを作る</h4>
<p>VPCを作るときに、上のアーキテクチャの図のように、二つのAZそれぞれにpublic subnet 1つとprivate subnet 1つが作られるように設定する。今回はS3は使ってないので、作成しなくてもいいです。</p>
<h4>(2) EC2インスタンスを立てる</h4>
<p>二つのパブリックサブネットそれぞれに、一台ずつのEC2インスタンスを立てる。EC2作成時に、先ほど作ったVPCを選択すると、そのVPC内のパブリックサブネットが選択できるようになります。Auto-assign public IPをEnableにすることによって、グローバルIPアドレスが割り当てられるようにしてください。</p>
<p>セキュリティグループについて、全てのIPアドレス（0.0.0.0/0）からのhttp, https通信を許可してください。また、My IPからのssh通信を許可してください。</p>
<h4>(3) ALBを立てる</h4>
<p> ALBを作成する時、今回使用するVPCを選択する。サブネットのMappingsとして、VPC内の二つのパブリックサブネットを選択する。</p>
<p>セキュリティグループは、EC2インスタンスと同じでいいと思います。</p>
<p> そして、先ほど作った二つのEC2インスタンスをルーティングのターゲットとします。ルーティングのターゲットとなるTarget Groupは、あらかじめ作成しておいてください。Target Group作成時に選択するTarget Typeは、instancesでいいと思います。二つのEC2インスタンスを選択して、Target Groupを作成してください。</p>
<h4>(4) RDSを立てる</h4>
<p> Engine TypeはMySQLでいいと思います。</p>
<p> 今回は、Multi-AZ DB instanceを選択します。</p>
<p>ユーザ名とマスターパスワードは、EC2インスタンスからRDSにアクセスする時に必要になります。</p>
<p>Connectivityのcompute resourceの選択で、EC2インスタンスを一つ選択します。EC2インスタンス二つと接続したいですが、一つしか選べないので、とりあえず、一つ目のEC2インスタンスを選択します。後で、もう一つのEC2インスタンスがRDSへ接続できるように、一つ目のEC2インスタンスが持っているセキュリティグループを加えます。</p>


<h3>手順（EC2インスタンスにSSH接続をして、ウェブAPIを構築する）</h3>
<h4>(1) EC2インスタンスにSSH接続する</h4>

<p>下記のコマンドで、SSH接続をする。</p>
<p>yourkey.pemは、鍵のパスです。xxx.xxx.xxx.xxxは、EC2インスタンスのグローバルIPアドレスです。</p>

```ubuntu
sudo ssh -i yourkey.pem ubuntu@xxx.xxx.xxx.xxx
```

<h4>(2) dockerをインストールする。</h4>

https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository

<p>下記のように、dockerをインストールする。上記の公式ドキュメントに書いてあるコマンドをそのまま使った。</p>
<p>Add Docker's official GPG key:</p>

```ubuntu
$ sudo apt-get update
$ sudo apt-get install ca-certificates curl gnupg
$ sudo install -m 0755 -d /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
$ sudo chmod a+r /etc/apt/keyrings/docker.gpg
```
<p>Add the repository to Apt sources:</p>

```ubuntu
$ echo \
  "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
$ sudo apt-get update
```
```ubuntu
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
<p>下記で正常にインストールされたことを確認する。</p>

```ubuntu
$ sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
...
Hello from Docker!
This message shows that your installation appears to be working correctly.
```


<h4>(3) docker-composeをインストールする。</h4>

https://docs.docker.jp/v1.12/compose/install.html

<p>下記のように、docker-composeをインストールする。上記のリンクに書いてあるコマンドと基本的には同じで、バージョンだけ変更した。今回は、v2.22.0をインストールする。</p>
<p>下記がうまくいかなかったら、下記を実行する前に、「sudo -i」を実行して、次に下記からsudoを抜いたコマンドを実行してください。</p>

```ubuntu
$ sudo curl -L "https://github.com/docker/compose/releases/download/v2.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
```

<p>下記で、docker-composeが正常にインストールされているかどうかを確認する。</p>

```ubuntu
$ docker-compose --version
Docker Compose version v2.22.0
```

<h4>(4) プロジェクトをgit cloneする</h4>
<p>docker-composeのプロジェクトがGitHubにある前提で話を進める。下記のようにgit cloneする。</p>
<p>今回は、「/var/www」フォルダを作り、そこにプロジェクトを配置する。</p>
<p>「YourName」はあなたのGithubアカウントの名前、「VPC-RDS-Test」は今回使うプロジェクトのレポジトリの名前を書いてください。</p>

```ubuntu
/var$ sudo mkdir www
/var$ cd www
/var/www$ sudo git clone https://github.com/YourName/VPC-RDS-Test
...
```
<h4>(5) プロジェクトの中身を確認</h4>
<p>今回、プロジェクトの階層構造や、ファイルの中身は下記のようになっている</p>

```ubuntu
ubuntu@ip-10-0-9-22:/var/www/VPC-RDS-Test$ ls -l
total 20
... Dockerfile
... app.py
... docker-compose.yml
... nginx.conf
... requirements.txt
```

```app.py
from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy
import os

app = Flask(__name__)

# RDSデータベース設定
db_user = 'YourName'
db_password = 'YourPassword'
db_host = 'YourRdsEndpoint'
db_name = 'test_db'
app.config['SQLALCHEMY_DATABASE_URI'] = f'mysql+pymysql://{db_user}:{db_password}@{db_host}/{db_name}'

db = SQLAlchemy(app)

# モデル定義
class Item(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)

    def __repr__(self):
        return '<Item %r>' % self.name

# データベース初期化とサンプルデータの追加
@app.before_first_request
def create_tables():
    db.create_all()
    # データが存在しない場合のみ追加
    if not Item.query.first():
        db.session.add(Item(name="Sample Item 1"))
        db.session.add(Item(name="Sample Item 2"))
        db.session.commit()

# ルートエンドポイント
@app.route('/')
def index():
    items = Item.query.all()
    return jsonify([item.name for item in items])

if __name__ == '__main__':
    app.run(host='0.0.0.0')
```
```docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "5000:5000"
  nginx:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
```
``` Dockerfile
# Dockerfile
FROM python:3.8-slim
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
CMD ["flask", "run"]
```
``` nginx.conf
events {}
http {
    server {
        listen 80;
        location / {
            proxy_pass http://app:5000;
        }
    }
}
```
``` requirements.txt
Flask==2.1.2
pymysql==1.0.2
SQLAlchemy==1.4
flask_sqlalchemy==2.5.1
Werkzeug==2.2.2
cryptography==3.4.7
```


<h4>(6) RDSに今回使うDBを作成する</h4>
<p>今回使うDBの名前は「test_db」です。EC2インスタンスからRDSにアクセスして、DBを作成します。下記はEC2インスタンスで実行するコマンドです。</p>
<p>「YourRdsEndpoint」は、RDSのドメイン名(Endpoint)を書いてください。「YourName」には、RDSを作成したときに決めた名前を書いてください。パスワードの入力が求められますが、RDSを作成したときに決めたパスワードを書いてください。</p>

```ubuntu
$ sudo apt update
$ sudo apt install mysql-server 
$ systemctl status mysql
...
Active: active (running) 
...
$ mysql -h YourRdsEndpoint -P 3306 -u YourName -p
Enter password:
mysql> create database test_db;
Query OK, 1 row affected (0.04 sec)
...
```
<p>MySQLから抜けるには、exitコマンドを使います。</p>

<h4>(7) docker-composeでビルドして、ウェブサイトにアクセスする</h4>
<p>下記でwebコンテナを起動させている間、このEC2インスタンスのグローバルIPアドレスにアクセスすると、ブラウザに["Sample Item 1", "Sample Item 2"]という文字列が表示されると思います。これは、webコンテナがDBにアクセスして、データを取得できていることを示します。</p>

```ubuntu
/var/www/VPC-RDS-Test$ sudo docker-compose build
...
/var/www/VPC-RDS-Test$ sudo docker-compose up
...
vpc-rds-test-app-1    |  * Running on all addresses (0.0.0.0)
vpc-rds-test-app-1    |  * Running on http://127.0.0.1:5000
vpc-rds-test-app-1    |  * Running on http://172.18.0.2:5000
vpc-rds-test-app-1    | Press CTRL+C to quit
...
```
<p>下記のようなウェブ画面が表示されると思います。下記は、EC2インスタンスに直接アクセスしています。</p>

![Untitled (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508161/36a3187f-c070-b291-bc0f-7e0f3f050447.png)


<p>ctrl+cでwebコンテナの実行を止めて、下記を改めて実行すると、バックグラウンドでの実行ができます</p>

```ubuntu
/var/www/VPC-RDS-Test$ sudo docker-compose up -d
...
```
<h4>(8) もう一つのEC2インスタンスをセットアップ</h4>
<p>次に、もう一つのEC2インスタンスに対して同じことを実行します。セキュリティグループが、一つ目のEC2インスタンスと同じものを持っているようにしてください。これにより、もう一つのEC2インスタンスがRDSにアクセスすることが可能になるはずです。</p>
<p>ALBのDNS NAME（ドメイン名）にアクセスすると、EC2インスタンスにルーティングされて、上記の画像と同じ表示になるはずです。</p>


<h4>(9) ALBがロードバランスしていることを確かめる</h4>
<p>Multi AZ構成を採用しているので、ALBを使っているが、ALBが正常に機能していることを確かめたい。ALBのルールに、「/path1」でアクセスされたら1つ目のEC2インスタンスにルーティングして、「/path2」でアクセスされたら2つ目のEC2インスタンスにルーティングするというルールを加える。GUIでALBの設定を変更してください。ALBのAdd listenerから条件を加えることができます。</p>
<p>スクリプトは以下のように変更します。</p>
<p>1つ目のEC2インスタンスにあるapp.pyに下記を追加して再ビルドする。/path1でアクセスされたら、ALBによって1つ目のEC2インスタンスにルーティングされて、そのwebコンテナはDBの1行目を取得して表示する。</p>

```app.py
@app.route('/path1')
def path1():
    item = Item.query.filter_by(id=1).first()
    return item.name
```

<p>2つ目のEC2インスタンスにあるapp.pyに下記を追加して再ビルドする。/path2でアクセスされたら、ALBによって2つ目のEC2インスタンスにルーティングされて、そのwebコンテナはDBの2行目を取得して表示する。</p>

```app.py
@app.route('/path2')
def path2():
    item = Item.query.filter_by(id=2).first()
    return item.name
```

<p>"/path1", "/path2"にアクセスして、意図通りのデータがブラウザに表示されていれば、ALBのルーティングが機能していることが分かる。"/path1"にアクセスすると、"Sample Item 1"が表示されて、"/path2"にアクセスすると、"Sample Item 2"が表示されるようになっているはず。</p>
<p>①"{ALBのドメイン名}" にアクセスしたときのウェブ画面は下図です。この時、ALBを介して、どちらかのEC2インスタンスに到達している。</p>

![Untitled (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508161/4323b79a-f663-0e1a-7564-c7968c792e13.png)


<p>②"{ALBのドメイン名}/path1" にアクセスしたときのウェブ画面は下図です。この時、ALBを介して、一つ目のEC2インスタンスに到達している。</p>

![Untitled (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508161/20dff0d8-ed50-a437-bc48-649e06b075ef.png)

<p>③"{ALBのドメイン名}/path2" にアクセスしたときのウェブ画面は下図です。この時、ALBを介して、二つ目のEC2インスタンスに到達している。</p>

![Untitled (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508161/e1ebe56d-483b-1dd1-fc22-f58b1a57b1f6.png)

<h2>今後の展望</h2>
<p>典型的なウェブアプリのVPC構造を構築できたと思うので、次は、Terraformとかを使ってみる。</p>

<h2>参考</h2>
<ul>
<li>https://www.rworks.jp/cloud/aws/aws-column/aws-entry/22067/</li>
</ul>