AWS

Apache/Python(uWSGI)/RDS(Aurora)で3層アーキテクチャ

スポンサーリンク
AWS
スポンサーリンク
スポンサーリンク

Apache/Python(uWSGI)/RDS(Aurora)で3層アーキテクチャ

簡単なWebアプリを構築することを考えます。

アプリは以下の3層構成とします。

  • Web層:ALB・Webサーバ(Auto Scaling)
  • アプリ層:NLB・アプリサーバ(Auto Scaling)
  • DB層:RDS(Aurora)

作成するアプリケーションは日時情報を保存・表示するアプリです。
クライアントからアクセスを受けた日時を保存するページと、保存されている日時の一覧を返すページを作成します。

構築する環境

プライベートサブネット内にWebサーバ用のAuto Scalingを作成します。
Auto Scalingグループ内に作成されるEC2インスタンスは、最新版のAmazon Linux 2とします。
グループ内のインスタンスは、Apacheをインストールすることで、Webサーバとして動作させます。
Auto ScalingグループにALBをアタッチします。

プライベートサブネット内にアプリサーバ用のAuto Scalingを作成します。
こちらのグループ内に作成されるインスタンスも、最新版のAmazon Linux 2とします。
インスタンスはPython(uWSGI)でアプリサーバとして動作させます。
Auto ScalingグループにNLBをアタッチします。

プライベートサブネットにRDS(Aurora)を作成します。
AuroraはDBサーバとして動作します。
AuroraはMySQLタイプとします。

CloudFormationカスタムリソースでAuroraの初期化処理を行います。
具体的にはカスタムリソースにLambda関数を関連付けて、DB内にテーブル作成を実行します。

パブリックサブネットにNATゲートウェイを配置します。
各種サーバの構築時に、インターネットにアクセスし、セットアップに必要なパッケージをダウンロードするために使用します。

クライアントと各種サーバ間の通信を整理します。

  • クライアント -> ALB -> Webサーバ:HTTP(80/tcp)
  • Webサーバ -> NLB -> アプリサーバ:UNIX Domain Socket(9090/tcp)
  • アプリサーバ -> DBサーバ:MySQL(3306/tcp)

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

awstut-fa/094 at main · awstut-an-r/awstut-fa
Contribute to awstut-an-r/awstut-fa development by creating an account on GitHub.

テンプレートファイルのポイント解説

ALB

今回の構成では、ALBをプライベートサブネット内のAuto Scalingグループにアタッチします。
こちらの詳細につきましては、以下のページをご確認ください。

セキュリティグループ

Resources: ALBSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${Prefix}-ALBSecurityGroup" GroupDescription: Allow HTTP Only. VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: !Ref HTTPPort ToPort: !Ref HTTPPort CidrIp: 0.0.0.0/0
Code language: YAML (yaml)

クライアントからWebサーバへの通信は、不特定多数のグローバルアドレスからHTTP(80/tcp)で行われます。
ですからこれを許可するようにルールを定義します。

Webサーバ

Auto Scalingグループ

Resources: LaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: LaunchTemplateData: IamInstanceProfile: Arn: !Ref InstanceProfileArn ImageId: !Ref InstanceImageId InstanceType: !Ref InstanceType SecurityGroupIds: - !Ref WebSecurityGroup TagSpecifications: - ResourceType: instance Tags: - Key: !Ref InstanceTagKey Value: !Ref InstanceTagValueWeb LaunchTemplateName: !Sub "${Prefix}-LaunchTemplate-Web"
Code language: YAML (yaml)

Auto Scalingグループ内にWebサーバを配置します。

今回はスケーリングポリシーなしのAuto Scalingグループを構成します。
こちらに関する詳細は、以下のページをご確認ください。

Auto Scalingグループ用の起動テンプレートですが、ポイントはタグ設定です。
以下の通り、タグを設定します。

  • キー:Server
  • 値:ApacheWeb

これは後述するインスタンスの初期化処理時に、SSMドキュメントを使用するのですが、その際にタグ情報を使用して、対象インスタンスを区別するためです。

セキュリティグループ

Resources: WebSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${Prefix}-WebSecurityGroup" GroupDescription: Allow HTTP from ALBSecurityGroup. VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: !Ref HTTPPort ToPort: !Ref HTTPPort SourceSecurityGroupId: !Ref ALBSecurityGroup
Code language: YAML (yaml)

クライアントからALBに向けられたHTTPトラフィックは、Webサーバにルーティングされます。
トラフィックの送信元はALBからとなりますので、ALBのセキュリティグループを指定します。

SSM関連付け

Resources: WebServerAssociation: Type: AWS::SSM::Association Properties: AssociationName: !Sub "${Prefix}-run-shellscript-association" Name: AWS-RunShellScript OutputLocation: S3Location: OutputS3BucketName: !Ref PlaybookBucket OutputS3KeyPrefix: !Sub "${Prefix}/shellscript-association-log" Parameters: commands: - yum update -y - yum install -y httpd - !Sub "echo 'ProxyPass / uwsgi://${NLBDNSName}:${UWSGIPort}/' >> /etc/httpd/conf/httpd.conf" - !Sub "echo 'ProxyPassReverse / uwsgi://${NLBDNSName}:${UWSGIPort}/' >> /etc/httpd/conf/httpd.conf" - systemctl start httpd - systemctl enable httpd Targets: - Key: !Sub "tag:${InstanceTagKey}" Values: - !Ref InstanceTagValueWeb WaitForSuccessTimeoutSeconds: !Ref WaitForSuccessTimeoutSeconds
Code language: YAML (yaml)

インスタンスをWebサーバとして動作させるための初期化処理を定義します。
今回はSSMドキュメントAWS-RunShellScriptを使用します。
SSMドキュメントを使用したインスタンスの初期化処理については、以下のページをご確認ください。

実行する内容は以下の通りです。

  • Apacheをインストールする。
  • 設定ファイル(httpd.conf)にNLB向けのプロキシ設定を追記する。
  • Apacheを起動・有効化する。

Targetsプロパティで、この初期化処理を実行する対象を指定します。
先述のタグが付与されているインスタンスに対して、このSSM関連付けを適用します。

NLB

今回の構成では、Webサーバとアプリサーバを分離し、両サーバの間に内部NLBを配置します。
本構成に関する詳細は、以下のページをご確認ください。

アプリサーバ

Auto Scalingグループ

Resources: LaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: LaunchTemplateData: IamInstanceProfile: Arn: !Ref InstanceProfileArn ImageId: !Ref InstanceImageId InstanceType: !Ref InstanceType SecurityGroupIds: - !Ref AppSecurityGroup TagSpecifications: - ResourceType: instance Tags: - Key: !Ref InstanceTagKey Value: !Ref InstanceTagValueApp LaunchTemplateName: !Sub "${Prefix}-LaunchTemplate-App"
Code language: YAML (yaml)

Auto Scalingグループ内にWebサーバを配置します。

先述の通り、ポイントはタグ設定です。
以下の通り、タグを設定します。

  • キー:Server
  • 値:ApacheWeb

セキュリティグループ

Resources: AppSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${Prefix}-AppSecurityGroup" GroupDescription: Allow uWSGI from NLB. VpcId: !Ref VPC
Code language: YAML (yaml)

アプリサーバ用のセキュリティグループですが、CloudFormationテンプレート上では、許可する通信を指定しません。
アプリサーバ向けトラフィックの送信元はNLBですが、今回のNLBはクライアントIPの保存を無効化しているため、トラフィックの送信元はNLBのプライベートアドレスとなります。
そしてこのアドレスはNLBの作成後に動的に付与されるため、テンプレートでは指定することはできないものとなります。

上記への対応として、CloudFormationカスタムリソースを使用します。
詳細は以下のページご確認ください。

今回は上記のページの内容の内、Lambda関数を以下の通りに変更します。

Resources: AppSecurityGroupFunction: Type: AWS::Lambda::Function Properties: Code: ZipFile: | import boto3 import cfnresponse import os nlb_loadbalancer_name = os.environ['NLB_LOADBALANCER_NAME'] security_group_id = os.environ['SECURITY_GROUP'] uwsgi_port = int(os.environ['UWSGI_PORT']) filter_value = '*{nlb}*'.format(nlb=nlb_loadbalancer_name) client = boto3.client('ec2') CREATE = 'Create' response_data = {} def lambda_handler(event, context): try: if event['RequestType'] == CREATE: describe_network_interfaces_response = client.describe_network_interfaces( Filters=[ { 'Name':'description', 'Values':[ filter_value ] } ] ) for interface in describe_network_interfaces_response['NetworkInterfaces']: private_address = interface['PrivateIpAddress'] authorize_security_group_ingress_response = client.authorize_security_group_ingress( GroupId=security_group_id, IpPermissions=[ { 'FromPort': uwsgi_port, 'IpProtocol': 'tcp', 'IpRanges': [ { 'CidrIp': '{address}/32'.format(address=private_address) } ], 'ToPort': uwsgi_port } ] ) print(authorize_security_group_ingress_response) cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) except Exception as e: print(e) cfnresponse.send(event, context, cfnresponse.FAILED, response_data) Environment: Variables: NLB_LOADBALANCER_NAME: !Ref NLBLoadBalancerName SECURITY_GROUP: !Ref AppSecurityGroup UWSGI_PORT: !Ref UWSGIPort FunctionName: !Sub "${Prefix}-AppSecurityGroupFunction" Handler: !Ref FunctionHandler Runtime: !Ref FunctionRuntime Role: !GetAtt AppSecurityGroupFunctionRole.Arn
Code language: YAML (yaml)

NLBのプライベートアドレスを取得後、これを使用して、アプリサーバ用セキュリティグループのルールを追加します。

SSM関連付け

Resources: AppServerAssociation: Type: AWS::SSM::Association Properties: AssociationName: !Sub "${Prefix}-apply-ansible-playbook-association" Name: AWS-ApplyAnsiblePlaybooks OutputLocation: S3Location: OutputS3BucketName: !Ref PlaybookBucket OutputS3KeyPrefix: !Sub "${Prefix}/playbook-association-log" Parameters: Check: - "False" ExtraVariables: - !Sub >- SSM=True DB_NAME=${DBName} DB_PASSWORD=${DBPassword} DB_READ_ENDPOINT_ADDRESS=${DBReadEndpointAddress} DB_TABLENAME=${DBTableName} DB_USER=${DBUser} DB_WRITE_ENDPOINT_ADDRESS=${DBWriteEndpointAddress} MYSQL_PORT=${MySQLPort} InstallDependencies: - "True" PlaybookFile: - !Ref PlaybookFileName SourceInfo: - !Sub '{"path": "https://${PlaybookBucket}.s3.${AWS::Region}.amazonaws.com/${Prefix}/${PlaybookPackageName}"}' SourceType: - S3 Verbose: - -v Targets: - Key: !Sub "tag:${InstanceTagKey}" Values: - !Ref InstanceTagValueApp WaitForSuccessTimeoutSeconds: !Ref WaitForSuccessTimeoutSeconds
Code language: YAML (yaml)

インスタンスをWebサーバとして動作させるための初期化処理を定義します。
今回はSSMドキュメントAWS-ApplyAnsiblePlaybooksを使用します。
後述のAnsible Playbookを実行することで、初期化を実行します。

ポイントはExtraVariablesプロパティです。
本プロパティで変数を定義することができますが、これらの変数はAnsibleのランタイム環境で使用することができます。
今回は本プロパティを使用して、DBのユーザ名・パスワード等の情報をAnsible環境に渡します。
記法は以下の通りです。

[変数名1]=[値1] [変数名2]=[値2] …

今回はコードの可読性を高めるために、変数を複数行に分けて記載します。
「>-」を使用することで、各行の改行をスペースに変換して結合しつつ、最終行の改行は削られます。

playbook.yml・uwsgi.ini・uwsgi.service

基本的には、以下のページでご紹介した内容と同様です。

playbook.yml

playbook.ymlに変更点があるため、これを取り上げます。

- hosts: all gather_facts: no become: yes tasks: - name: update yum. yum: name=* - name: install packages by yum. yum: name: - python3-devel - gcc - name: install packages by pip3. pip: name: - uwsgi - flask - mysql-connector-python executable: pip3 - name: create app directory. file: path: /home/ec2-user/myapp state: directory - name: copy flask script. copy: src: ./run.py dest: /home/ec2-user/myapp/run.py - name: copy uWSGI ini. copy: src: ./uwsgi.ini dest: /home/ec2-user/myapp/uwsgi.ini - name: copy uWSGI Service. copy: src: ./uwsgi.service dest: /etc/systemd/system/uwsgi.service - name: create uWSGI environment variables file. copy: dest: "/etc/sysconfig/uwsgi" content: | DB_NAME={{DB_NAME}} DB_PASSWORD={{DB_PASSWORD}} DB_READ_ENDPOINT_ADDRESS={{DB_READ_ENDPOINT_ADDRESS}} DB_TABLENAME={{DB_TABLENAME}} DB_USER={{DB_USER}} DB_WRITE_ENDPOINT_ADDRESS={{DB_WRITE_ENDPOINT_ADDRESS}} MYSQL_PORT={{MYSQL_PORT}} - name: reload daemon. systemd: daemon_reload: yes - name: start and enable uWSGI. systemd: name: uwsgi state: started enabled: yes
Code language: YAML (yaml)

systemd用の変数を定義するタスクを追加しました。
/etc/sysconfig/uwsgiにファイルを作成し、これに環境変数を記述します。
作成する変数はDBのユーザ名・パスワード等の情報です。
これらはSSM関連付けのExtraVariablesで定義され、Ansibleに渡された値を使用しています。

uwsgi.service

uwsgi.serviceで、/etc/sysconfig/uwsgiを読み込むように設定します。

[Unit] Description = uWSGI After = syslog.target [Service] WorkingDirectory = /home/ec2-user/myapp/ EnvironmentFile=/etc/sysconfig/uwsgi ExecStart = /usr/local/bin/uwsgi --ini /home/ec2-user/myapp/uwsgi.ini Restart = on-failure RestartSec = 3 KillSignal = SIGQUIT Type = notify StandardError = syslog NotifyAccess = all [Install] WantedBy = multi-user.target
Code language: plaintext (plaintext)
run.py
import datetime import json import mysql.connector import os from flask import Flask db_name = os.environ['DB_NAME'] db_password = os.environ['DB_PASSWORD'] db_read_endpoint_address = os.environ['DB_READ_ENDPOINT_ADDRESS'] db_tablename = os.environ['DB_TABLENAME'] db_user = os.environ['DB_USER'] db_write_endpoint_address = os.environ['DB_WRITE_ENDPOINT_ADDRESS'] mysql_port = int(os.environ['MYSQL_PORT']) str_format = '%Y-%m-%d %H:%M:%S' app = Flask(__name__) @app.route('/') @app.route('/read') def read(): conn = mysql.connector.connect( host=db_read_endpoint_address, port=mysql_port, user=db_user, password=db_password, database=db_name ) cur = conn.cursor() read_sql = 'select * from {table};'.format(table=db_tablename) cur.execute(read_sql) result = json.dumps( [record[0].strftime(str_format) for record in cur], indent=2 ) cur.close() conn.close() return result @app.route('/write') def write(): conn = mysql.connector.connect( host=db_write_endpoint_address, port=mysql_port, user=db_user, password=db_password, database=db_name ) cur = conn.cursor() now = datetime.datetime.now() now_str = now.strftime(str_format) write_sql = 'insert into {table} values ("{now}");'.format( table=db_tablename, now=now_str ) cur.execute(write_sql) cur.close() conn.commit() conn.close() return 'Saved: {now}'.format(now=now_str) if __name__ == '__main__': app.run()
Code language: Python (python)

Flaskを使用して、以下の2ページを定義します。

  • readページ・トップページ:Auroraに接続し、保存されている全日時情報を取得して、クライアントに返す。
  • writeページ:Auroraに接続し、現在の日時情報を保存し、クライアントに返す。

それぞれのページでAuroraに接続しますが、接続時に使用するエンドポイントが異なります。
readページの場合は、読み込み処理だけが発生しますので、リードレプリカ向けの読み込み用エンドポイントを使用します。
writeページの場合は、書き込み処理も発生しますので、プライマリサーバ向けの書き込み用エンドポイントを使用します。

DBサーバ

Aurora

Auroraクラスターにプライマリサーバとリードレプリカサーバを作成します。
基本的な構成は、以下のページと同様です。

AuroraのDBを初期化するために、CloudFormationカスタムリソースを使用します。
具体的には、カスタムリソースに紐づくLambda関数から、Auroraに接続し、初期化処理を実行します。
詳細につきましては、以下のページをご確認ください。

今回はSSMパラメータストアに保存された、以下のSQL文を実行することで、DBの初期化を実行します。

Resources: SQLParameter: Type: AWS::SSM::Parameter Properties: Name: !Sub "${Prefix}-SQLParameter" Type: String Value: !Sub | USE ${DBName}; CREATE TABLE ${DBTableName} (dt datetime);
Code language: YAML (yaml)

具体的には、操作するDBを選択後、日時情報を保存するテーブルを作成する内容です。

セキュリティグループ

Resources: DBSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${Prefix}-DBSecurityGroup" GroupDescription: Allow MySQL from AppSecurityGroup and FunctionSecurityGroup. VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: !Ref MySQLPort ToPort: !Ref MySQLPort SourceSecurityGroupId: !Ref AppSecurityGroup - IpProtocol: tcp FromPort: !Ref MySQLPort ToPort: !Ref MySQLPort SourceSecurityGroupId: !Ref FunctionSecurityGroup
Code language: YAML (yaml)

Auroraへの接続はアプリサーバとLambda関数から発生しますので、これらの通信を許可する内容です。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

Ansible Playbookの準備

CloudFormationスタックを作成する前に、Ansibleの準備を行います。
具体的にはPlaybook等をZip化し、S3バケットに設置します。具体的なコマンドは以下のページをご確認ください。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • ALB:fa-094-ALB
  • ALBのDNS名:fa-094-alb-1763061004.ap-northeast-1.elb.amazonaws.com
  • NLB:fa-094-NLB
  • Webサーバ用Auto Scaling:fa-094-ALBAutoScalingGroup
  • アプリサーバ用Auto Scaling:fa-094-NLBAutoScalingGroup
  • Auroraクラスター:fa-094-dbcluster

ALB

作成されたリソースをAWS Management Consoleから確認します。
ALBを確認します。

ALBが正常に作成されていることがわかります。
またALBのDNS名もわかります。

ALBターゲットグループも確認します。

グループ内に2つのインスタンスが作成されていることがわかります。
つまりALBに関連づいているWeb用Auto Scalingグループによって、自動的にWebサーバインスタンスが2台起動したということです。

NLB

NLBを確認します。

NLBが正常に作成されていることがわかります。

NLBターゲットグループも確認します。

グループ内に2つのインスタンスが作成されていることがわかります。
つまりNLBに関連づいているアプリ用Auto Scalingグループによって、自動的にアプリサーバインスタンスが2台起動したということです。

Aurora

Auroraクラスターを確認します。

正常にAuroraクラスターが作成されていることがわかります。
確かに書き込み用・読み込み用のエンドポイントが作成されていることがわかります。

動作確認

準備が整いましたので、ALBのトップページにアクセスします。

レスポンスとして空のリストが返ってきました。
まだ日時情報が保存されていないためです。

何度か書き込みページにアクセスし、DBにデータを保存させます。

正常に日時データが保存されました。

改めて読み込みページにアクセスします。

保存されたデータが返ってきました。
今回の3層構造のアプリが正常に動作していることがわかります。

(参考)サーバのログ

参考までにWeb・アプリ層のサーバログを確認します。

まず2台のWebサーバのログの一部を取り上げます。

10.0.2.56 - - [06/Nov/2022:03:01:52 +0000] "GET /write HTTP/1.1" 200 26 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" 10.0.2.56 - - [06/Nov/2022:03:02:03 +0000] "GET / HTTP/1.1" 200 77 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
Code language: plaintext (plaintext)
10.0.2.56 - - [06/Nov/2022:03:01:35 +0000] "GET / HTTP/1.1" 200 2 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" 10.0.2.56 - - [06/Nov/2022:03:01:47 +0000] "GET /write HTTP/1.1" 200 26 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" 10.0.2.56 - - [06/Nov/2022:03:01:57 +0000] "GET /write HTTP/1.1" 200 26 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" 10.0.2.56 - - [06/Nov/2022:03:02:14 +0000] "GET /read HTTP/1.1" 200 77 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
Code language: plaintext (plaintext)

ALBによって、2つのWebサーバにトラフィックが振り分けられていることがわかります。

次にアプリサーバのログの一部を取り上げます。

[pid: 2178|app: 0|req: 70/70] 10.0.2.56 () {64 vars in 1335 bytes} [Sun Nov 6 03:01:47 2022] GET /write => generated 26 bytes in 48 msecs (HTTP/1.1 200) 2 headers in 79 bytes (1 switches on core 0) [pid: 2178|app: 0|req: 71/71] 10.0.1.106 () {52 vars in 765 bytes} [Sun Nov 6 03:01:47 2022] GET / => generated 27 bytes in 26 msecs (HTTP/1.1 200) 2 headers in 79 bytes (1 switches on core 0) [pid: 2178|app: 0|req: 73/73] 10.0.2.56 () {64 vars in 1332 bytes} [Sun Nov 6 03:02:14 2022] GET /read => generated 77 bytes in 30 msecs (HTTP/1.1 200) 2 headers in 79 bytes (1 switches on core 0)
Code language: plaintext (plaintext)
[pid: 2189|app: 0|req: 67/67] 10.0.1.106 () {52 vars in 762 bytes} [Sun Nov 6 03:01:47 2022] GET / => generated 27 bytes in 44 msecs (HTTP/1.1 200) 2 headers in 79 bytes (1 switches on core 0) [pid: 2189|app: 0|req: 68/68] 10.0.2.56 () {66 vars in 1365 bytes} [Sun Nov 6 03:01:52 2022] GET /write => generated 26 bytes in 34 msecs (HTTP/1.1 200) 2 headers in 79 bytes (1 switches on core 0) [pid: 2189|app: 0|req: 69/69] 10.0.2.56 () {66 vars in 1366 bytes} [Sun Nov 6 03:01:57 2022] GET /write => generated 26 bytes in 37 msecs (HTTP/1.1 200) 2 headers in 79 bytes (1 switches on core 0)
Code language: plaintext (plaintext)

こちらもNLBによって、2つのアプリサーバにトラフィックが振り分けられていることがわかります。

まとめ

Apache/Python(uWSGI)/RDS(Aurora)で3層アーキテクチャを作成しました。

タイトルとURLをコピーしました