GitEngHar / GrowTheLatestTechnorogy

0 stars 2 forks source link

AWS Fargateでビジネスユースケースを実現したい #31

Open GitEngHar opened 1 year ago

GitEngHar commented 1 year ago

目的

現在諸事情で「AWS ECS」を学ぼうとなっている
公式Docs / 本 / ブログ を参考にしながら ECR へ image を push し image を ECSでサービス として 動かし、外部からアクセスできた
その過程で より 「セキュリティ性の高い構成」 と 「実用性の高い構成」を組めるようになりたいと感じた
上記を 模擬的なビジネスユースケース を 考え 構築していくことで 実現したい

目標

物凄く リッチ な自己紹介ページを作る

何を持って達成とするか

以下項目の全てにチェックが入っていること

満たすべき技術要件

基本技術要件(やることなくなったら確認) - [ ] AWSアカウントの設定: - [ ] 触ったことがある - [ ] ドキュメントに目を通した - [ ]知らなかったことで知れたことをまとめられている - [ ] AWSのリージョンとアベイラビリティゾーン: - [ ] 触ったことがある - [ ] ドキュメントに目を通した - [ ]知らなかったことで知れたことをまとめられている - [ ] EC2インスタンスの作成: - [ ] 触ったことがある - [ ] ドキュメントに目を通した - [ ]知らなかったことで知れたことをまとめられている - [ ] S3バケットの作成: - [ ] 触ったことがある - [ ] ドキュメントに目を通した - [ ]知らなかったことで知れたことをまとめられている - [ ] VPCの設定: - [ ] 触ったことがある - [ ] ドキュメントに目を通した - [ ]知らなかったことで知れたことをまとめられている - [ ] セキュリティグループとネットワークACL: - [ ] 触ったことがある - [ ] ドキュメントに目を通した - [ ]知らなかったことで知れたことをまとめられている - [ ] IAMロールとアクセス許可: - [ ] 触ったことがある - [ ] ドキュメントに目を通した - [ ]知らなかったことで知れたことをまとめられている - [ ] データベースのセットアップ: - [ ] 触ったことがある - [ ] ドキュメントに目を通した - [ ]知らなかったことで知れたことをまとめられている - [ ] モニタリングとログ: - [ ] 触ったことがある - [ ] ドキュメントに目を通した - [ ]知らなかったことで知れたことをまとめられている - [ ] コスト管理: - [ ] 触ったことがある - [ ] ドキュメントに目を通した - [ ]知らなかったことで知れたことをまとめられている
GitEngHar commented 1 year ago

Step1 どんなサイトにしたいか

サイト仕様

こんなデザインにしたい (最終的には)

Step2 imageを自作

Step3 webページを動かすコンテナを制作

こちらでStep2~3は実施済み

Step4 データベースとwebページの連携

Step5 CI/CDの連携

Step6 セキュリティグループとネットワークアクセス制御

Step7 コンテナイメージのセキュア化

Step8 セキュリティポリシー

Step9 AWSのベストプラクティスの確認:

GitEngHar commented 1 year ago

以下を実施してみる

■ ref : "【ポートフォリオをECSで!】Rails×NginxアプリをFargateにデプロイするまでを丁寧に説明してみた(VPC作成〜CircleCIによる自動デプロイまで) 前編 - Qiita" "https://qiita.com/maru401/items/8e7d32a8baded045adb2#41-%E3%82%BF%E3%82%B9%E3%82%AF%E3%81%AE%E4%BD%9C%E6%88%90"

image

■ ref : "MySQL DB インスタンスの作成と接続 - Amazon Relational Database Service" "https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/CHAP_GettingStarted.CreatingConnecting.MySQL.html"

まとめその1

GitEngHar commented 1 year ago

Ec2からMysqlに接続したい

RDSのmysqlデータベースのsgで許可しているsgをEc2に割り当て、mysqlに接続する

image

とりあえず成功

pythonで接続できるようにする

試しの実施で以下のコードでtest

import pymysql

connection=pymysql.connect(host='<endpoint>', port=3306, user='<user>', password='<pass>', db='<dbname>')

try:
    with connection.cursor() as cursor:
        sql = "select * from sampletable"
        cursor.execute(sql)
        result = cursor.fetchall()
        print(result)
finally:
    connection.close()

image

セキュアな通信は以下より実施できそう

IAM 認証および AWS SDK for Python (Boto3) を使用した DB インスタンスへの接続 - Amazon Relational Database Service

ここで気づいたが、Fargate複数立てる必要ないのでは..??
資格の登録をする場合、json形式で送ったデータをmysqlに書き込めるようにしたい → backendはpython or Go or js でリクエストを待ち受けるものにしたい

GitEngHar commented 1 year ago

これを作っていくZO! image

GitEngHar commented 1 year ago

PythonでPOST GETをjsonでやりとりできるAPIの作成

pyServerをすきなportでlistenしてみるの巻き

curlで指定しているURLは現在は固定だが、タスクとして実行した際は変動する。そこをどうするかは悩みどころ。 kubernetesではingressにresponseを送ることで何とかなっていたので同じような構成にしたい。

APIでjsonをやり取りしたい

socket通信でjson形式のデータをやり取りできたが、受け取ってる文字コードをdecodeして、テキストとして読み込んでいるため、jsonとしてのデータではないと感じた。 そのため、restAPIを構築してjson形式のデータをやり取りできるようにしたい

以下を参照して実施 PythonでAPIを爆速で構築してみた - Qiita

8000でlocal公開されているapiにcurlを実施。結果が返ってきた

ec2-user:~/environment/plactis/pyServer $ curl -X POST "localhost:8000/items" -H 'Content-Type: application/json' -d '{"name":"おなまえ","price":123}' 
{"item_name":"おなまえ","twice price":246.0}

FastAPIを利用してAPIの理解を深めたい

まずは慣れよう

GET

/fruitsで果物一覧を取得

$ curl -X GET "localhost:8000/fruits"
{"message":"orange, kiwii, grape, lemon, "

/colorで色の一覧を取得

 $ curl -X GET "localhost:8000/color"
{"message":{"pink":["aurora","begonia","camellia"],"red":["azalea","burgundy","carmine"],"orange":["apricot","carrot","nasturtium"]}}
POST

/bmi で 体重と身長からBMIを計算する

$ curl -X POST "localhost:8000/bmi" -H 'Content-Type: application/json' -d '{"name":"haru","h":1.7,"w":70}'
{"name":"haru","bmi":24.221453287197235}

/literalnum で文字数を教えてくれるやつ

 $ curl -X POST "localhost:8000/strnum" -H 'Content-Type: application/json' -d '{"string":"こんにちは。文字の長さを教えてください"}'
{"strNumber":19}

FAST APIについて知ろう

FastAPI

FASTAPIはPythonでAPIを構築できるwebフレームワーク Djangoと並行して人気らしい

app(/test) app(/test/calc)

上記のように複数pathごとに処理を変えられる

GitEngHar commented 1 year ago

Python APIでRDSから値を取得するAPIの作成

Mysql Server Query の動作テスト

AWS EC2にssh後外部との通信がうまくいかず、packege更新が一生終わらなかったため、 Docker Playground環境で実施

playgroundで必要だった環境構築

pip3 install pymysql
pip3 install fastapi
pip3 install pydantic
pip3 install uvicorn
pip install cryptography

動作確認用のコード

import pymysql

def connectDB():
    connection=pymysql.connect(host='<ip>', port=3306, user='root', password='password', db='SAMPLEDB')
    try:
        with connection.cursor() as cursor:
            sql = "select * from sampletb"
            cursor.execute(sql)
            result = cursor.fetchall()
            return result[0][0]
    finally:
        connection.close()

動作確認用コマンド

#msyql docker を構築
docker run --name mysqlDB -e MYSQL_ROOT_PASSWORD=password -p 3306:3306 -d mysql

#docker login
docker exec -it mysqlDB bash

以下を参考にデータベースをセットアップ MySQLチートシート - Qiita MySQL | CHAR型とVARCHAR型

Mysql Server Query から OUTPUTを解析

python でクエリした結果はtuple型で帰ってくる image

配列から抜き取る簡単な形式で値を取得

Mysql Query と perseをライブラリ化し API化する

簡単に以下のようになる

import mysqlConnect as connect
from fastapi import FastAPI

app = FastAPI()

@app.get("/connectTest")
async def root():
    uname = connect.connectDB()
    return {"message": uname}

実行結果

[node1] (local) root@192.168.0.28 ~
$ curl -X GET "localhost:8000/connectTest"
{"message":"haru"}
GitEngHar commented 1 year ago

BackendのImage化と環境変数の外だし

DataBaseのユースケース と MysqlDBの設計

Usecase

設計

DBの環境構築

#mysqlログイン
$ mysql -u root -p
#database作成
$ CREATE DATABASE SAMPLEDB;
$ USE SAMPLEDB;
#table作成
$ CREATE TABLE certtb(skillType varchar(255) , certName varchar(255));
#データを試しに登録
$ insert into certtb (skillType,certName) VALUE ('Linux','LPIClv1')
#登録データの確認
$ select * from sampletb

確認結果

mysql> select * from certtb;
+-----------+----------+
| skillType | certName |
+-----------+----------+
| Linux     | LPIlCv1  |
+-----------+----------+

PyAPIを用いてMysqlのユースケースごとの動作を作成する

backend.py

import mysqlConnect as connect
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    skilltype: str
    certname: str

@app.get("/getcert")
async def root():
    connect.viewDB()
    return {"message": "OK"}

@app.post("/regcert")

def update_item(item: Item):
    connect.regDB(item.skilltype,item.certname)
    return {"rsponse": "OK"}

mysqlConnect.py

import pymysql

connection=pymysql.connect(host='192.168.0.18', port=3306, user='root', password='password', db='SAMPLEDB')

def viewDB():
    print("!!!!!view function")
    sql = "select * from certtb"
    queryDB(sql)

def regDB(skillType,certName):
    sql = "insert into certtb(skillType,certName) VALUE ('{0}','{1}')".format(skillType,certName)
    print("exec : {0}".format(sql))
    queryDB(sql)

def queryDB(sql):    
    try:
        with connection.cursor() as cursor:
            cursor.execute(sql)
            result = cursor.fetchall()
            print(result)
            return 0
    finally:
        connection.close()

上記で localにmysql databaseが立っており、uvicorn backend:app --reload でpython APIも動作していれば、以下実行結果が得られる

mysql に 資格情報を登録

#資格情報を登録する要求 (OK が帰ってくれば登録できている 多分)
$ curl -X POST "localhost:8000/regcert" -H 'Content-Type: application/json' -d '{"skilltype":"Cisco","certname":"CCNA"}'
{"rsponse":"OK"}

msyql から資格情報を取得

$ curl -X GET "localhost:8000/getcert"
{"message":"OK"}

#Server側で取得したクエリデータを取得出きている(要改善)
(('Linux', 'LPIlCv1'), ('Linux', 'LPIClv2'))

取得した値をjson形式に成型

import mysqlConnect as connect
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

def createRespons(result):
    response = ""
    for i in range(len(result)):
        data = "skilltype:{0},certcame:{1}".format(result[i][0],result[i][1])
        response += "{" + data + "}"
        if len(result)-1 != i : response += ","
    response = "{" + response + "}"
    return response

class Item(BaseModel):
    skilltype: str
    certname: str

@app.get("/getcert")
async def root():
    result = connect.viewDB()
    return createRespons(result)

@app.post("/regcert")
def update_item(item: Item):
    connect.regDB(item.skilltype,item.certname)
    return {"rsponse": "OK"}

実行結果

$ curl -X GET "localhost:8000/getcert"
"{{skilltype:linux,certcame:LPIC1},{skilltype:linux,certcame:LPIC2}}"
GitEngHar commented 1 year ago

出力されるjsonを調整。insertでDBにデータが登録されない問題を解消

ソースコードたち

backend.py

import mysqlConnect as connect
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

def createTupleRespons(result):
    response = ""
    print(result)
    for i in range(len(result)):
        data = "skilltype:{0},certname:{1}".format(result[i][0],result[i][1])
        response += "{" + data + "}"
        if len(result)-1 != i : response += ","
    response = "{" + response + "}"
    return response

def createJsonResponse(result):
    print(type(result))
    data = "{0}".format(result)
    response = data.replace("[","{")
    response = response.replace("]","}")
    return response

class Item(BaseModel):
    skilltype: str
    certname: str

@app.get("/getcert")
async def root():
    result = connect.viewDB()
    if type(result) is tuple:
        response = createTupleRespons(result)
    elif type(result) is list: 
        response = createJsonResponse(result)
    return response

@app.post("/regcert")
def update_item(item: Item):
    connect.regDB(item.skilltype,item.certname)
    return {"rsponse": "OK"}

mysqlConnect.py

import pymysql

def viewDB():
    sql = "select * from certtb"
    sendSql = "{0}".format(sql)
    return queryDB(sql)

def regDB(skillType,certName):
    sql = "insert into certtb(skillType,certName) VALUE ('{0}','{1}')".format(skillType,certName)
    queryDB(sql)

def queryDB(sql):    
    connection=pymysql.connect(host='192.168.0.8', port=3306, user='root', password='password', db='SAMPLEDB', cursorclass=pymysql.cursors.DictCursor)
    try:
        with connection.cursor() as cursor:
            cursor.execute(sql)
            connection.commit()
            result = cursor.fetchall()
            print(result)
            return result
    finally:
        connection.close()

実行結果

[node1] (local) root@192.168.0.8 ~
$ curl -X POST "localhost:8000/regcert" -H 'Content-Type: application/json' -d '{"skilltype":"cisco","certname":"CCNA"}'
{"rsponse":"OK"}[node1] (local) root@192.168.0.8 ~
$  curl -X GET "localhost:8000/getcert"
"{{'skillType': 'linux', 'certName': 'LPIC1'}, {'skillType': 'linux', 'certName': 'LPIC3'}, {'skillType': 'linux', 'certName': 'LPIC2'}, {'skillType': 'cisco', 'certName': 'CCNA'}}"
GitEngHar commented 1 year ago

pyAPIの環境変数をosの環境変数で設定できるようにする & Image化

環境変数をosで設定できるようにする

ユースケースは RDS で使われること osで設定したい環境変数一覧

Code

import pymysql
import os

tbname = os.getenv("TB_NAME")
dbname = os.getenv("DB_NAME")
mysqlUser = os.getenv("MYSQL_USER_NAME")
mysqlUserPass = os.getenv("MYSQL_USER_PASSWORD")
mysqlEndpoint = os.getenv("MYSQL_ENDPOINT")

def viewDB():
    sql = "select * from {0}".format(tbname)
    return queryDB(sql)

def regDB(skillType,certName):
    sql = "insert into {0}(skillType,certName) VALUE ('{1}','{2}')".format(tbname,skillType,certName)
    queryDB(sql)

def queryDB(sql):    
    print("tbname:{0},dbname:{1},mysqlUser:{2},mysqlUserPass:{3},mysqlEnd:{4}".format(tbname,dbname,mysqlUser,mysqlUserPass,mysqlEndpoint))
    connection=pymysql.connect(host=mysqlEndpoint, port=3306, user=mysqlUser, password=mysqlUserPass, db=dbname, cursorclass=pymysql.cursors.DictCursor)
    try:
        with connection.cursor() as cursor:
            cursor.execute(sql)
            connection.commit()
            result = cursor.fetchall()
            return result
    finally:
        connection.close()

terminal

[node2] (local) root@192.168.0.17 ~
$ export TB_NAME=certtb
[node2] (local) root@192.168.0.17 ~
$ export DB_NAME=SAMPLEDB
[node2] (local) root@192.168.0.17 ~
$ export MYSQL_USER_NAME=root
[node2] (local) root@192.168.0.17 ~
$ export MYSQL_USER_PASSWORD=password
[node2] (local) root@192.168.0.17 ~
$ export MYSQL_ENDPOINT=192.168.0.8

実行結果は上記同様なので割愛だが、できた🎉

pyAPIをImage化する

imageを設計する

Docker File

from <python image>
cp アプリケーションたちとrequier.txt
pipでライブラリをそろえる
uvicornで起動※errorが出れば環境変数を設定しよう

imageを作成と動作テスト

docker上のアプリにlocalhostでアクセスしたらERR_EMPTY_RESPONSEが出る - Qiita

コンテナをlocalhost:portでlistenさせると、リクエストが届かない問題。 ホスト→コンテナネットワーク→コンテナ 上記のようなネットワークになる際、コンテナネットワークからコンテナへのリクエストはコンテナ内のアドレスに返還されるため、厳密にlocalhostのアドレス(127.0.0.1)ではない。そのため、リクエストが届かない。

■ リクエストが届いていない image

■コンテナ情報

ec2-user:~/environment $ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS          PORTS                                                  NAMES
5a2d9fdf5259   pyapiserv:v1   "uvicorn backend:app…"   9 minutes ago   Up 9 minutes    0.0.0.0:8000->8000/tcp, :::8000->8000/tcp              pyserv2
31263ae8c5f5   mysql          "docker-entrypoint.s…"   3 hours ago     Up 18 minutes   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysqlDB

解決! listen ipaddress が localhostとなっていたので、0.0.0.0に変更して成功 Internal Server Error で、サーバの処理失敗となっており、到達はした🎉 image

サーバの処理失敗はネットワークが異なることによる到達しないエラーかと思われる

失敗を受けて再度テスト

■ get成功

ec2-user:~/environment $ curl -X GET "localhost:8080/getcert"
"{{'skillType': 'cisco', 'certName': 'CCNA'}, {'skillType': 'Linux', 'certName': 'LPIClv1'}}"

■ server側もいい感じ image

■ post成功

ec2-user:~/environment $ curl -X POST "127.0.0.1:8080/regcert" -H 'Content-Type: application/json' -d '{"skilltype":"Linux","certname":"LPIClv3"}'
{"rsponse":"OK"}

■ dbに反映されていることを確認

ec2-user:~/environment $ curl -X GET "172.17.0.1:8080/getcert""{{'skillType': 'cisco', 'certName': 'CCNA'}, {'skillType': 'Linux', 'certName': 'LPIClv1'}, {'skillType': 'Linux', 'certName': 'LPIClv2'}, {'skillType': 'Linux', 'certName': 'LPIClv3'}}"

--ENDPOINTの指定を docker のipを指定したら成功した

ec2-user:~/environment $ docker inspect 31263ae8c5f5 | grep IPAddress      
            "SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.2",
                    "IPAddress": "172.17.0.2",
GitEngHar commented 1 year ago

AWS Fargateで動かそう

imageをECRへpush

ここに従ってimageをpush image

pushできていることを確認 image

RDS MYSQLの環境を整える

無限cloud9環境リロード問題を解消した素晴らしい記事 AWS Cloud9の環境作成が一生終わらない件について - Qiita

整え終わって..  image

リクエストを送る手段を考える

タスクのIPにリクエストを送る

できたー🎉

■ GET image

■ POST image

POSTできていたかをGETして確認 image

ALBで送る

苦労メモ

GitEngHar commented 1 year ago

サービス同士で通信しよう

backendにリクエストを送れる、簡易ウェブシステムを制作

postしてリクエストを表示するだけでええや

jsでリクエストを送りたいので、fetchを利用するが HTTP通信になるため、バックエンドでhttp通信のできるAPIが必要。 現在のままだとただ、パブリックIPにリクエストを投げてるにすぎないので、HTTPでリクエストを受け付けてくれる機能が必要。 L7LBのALBを使おう。そうしよう。

Curl成功 image

jsで取得とParse後 ブラウザのコンソールとhtmlに表示

フロント (仮) 完成🎉 image

コード

<!DOCTYPE html>
<html>

<body>
    <p>Helloword</p>
    <button id="clickEvent" onclick="getCertData();">リクエスト</button>
    <p id="view"></p>
    <script  language="javascript" type="text/javascript">
        async function  getCertData(){
            target = document.getElementById("view");
            var viewText = ""
            var response = await fetch('エンドポイント');
            if(!response.ok){
                viewText = "ResponseError";
            }
            viewText = ""
            const certRawJsonData = await response.json();
            const certJsonToStringData = JSON.stringify(certRawJsonData)
            var fixCertJsonTypeData = certJsonToStringData.replace(/'/g,"\"")
            fixCertJsonTypeData = fixCertJsonTypeData.replace("\"{{","[{")
            fixCertJsonTypeData = fixCertJsonTypeData.replace("}}\"","}]")
            const certJsonData = JSON.parse(fixCertJsonTypeData,(key,value) => {
                switch(key){
                    case "skillType":
                        viewText += `資格スキルの種類 : ${value} <br>`;
                        break;
                    case "certName":
                        viewText += `資格名 : ${value} <br><br>`
                }
            })
            target.innerHTML = viewText;
            console.log('response.json():', viewText);

        }
    </script>
</body>

</html>

内部通信用のALBを作成する

作成したが、サービスをプライベートかつ、パブリックIPを無効化していたら、ECRのImageがpullできなかった ECRのエンドポイントが作成されていないため、外部からのpullで取得している

調べたら S3 や Fargate や ECRのエンドポイント等々面倒くさいので、今回は ECSをパブリックにして、ALBはプライベートにして実施する → ALBを内部通信にすると、sgが難しい。パブリックでwebサーバを公開しているとwebサーバからのリクエストもパブリックになっている印象。そのため、sgでwebに適用されているsgを指定しても通らない

webシステムも同様にImage化する

Image化してFargateで動作できた🎉 次項を参照

service間で通信をして、mysqlを操作する

GET できた🎉 ちなみにFargateで公開しているタスクのパブリックIPに直接入っている

image

POSTまで行きたい

苦労したこと

最終的なざっくり構成

image

GitEngHar commented 1 year ago

ここまでのナレッジをまとめる

Fargateでタスクを実行してタスク間通信を行うところまで実施したので、 第3者がこの手順を追うことができるように作成しよう

  1. パブリックネットワークとプライベートネットワークを作成する
  2. ネットワーク周りを設定する
  3. プライベートにRDSを設置
  4. プライベートのSGを設定
  5. ECRにImageをPush
  6. クラスタの作成
  7. バックエンドサービスを作成&付属するALBと連携 (併せてSGも設定)
  8. フロントサービスを作成し、SGも作成
  9. 動作テスト

簡単な再現がしたいのであれば

CloudFormationで冪等性を保った構成をとってもよさそうだが..?? Imageが自作する必要ありなので、難しそう..??

GitEngHar commented 1 year ago

現在の構成 simpleWebapp drawio

参考にしたい構成 https://aws.amazon.com/jp/cdp/ec-container/ image