AWS

内部NLBを使用して、Web・アプリサーバを分離する – Apache版

スポンサーリンク
内部NLBを使用してWeb・アプリサーバを分離する - Apache版 AWS
スポンサーリンク
スポンサーリンク

内部NLBを使用して、Web・アプリサーバを分離する

ApacheとPythonでWebサービスを作成します。

Webサーバとアプリサーバを分離し、両サーバの間に内部NLBを配置することによって、両者を連携する方法を確認します。

構築する環境

Diagram of separating Web and App Servers Using Internal NLB - Apache Ver.

パブリックサブネットにEC2インスタンスを配置します。
インスタンスは最新版のAmazon Linux 2をベースにして作成します。
このインスタンスは、Apacheをインストールすることで、Webサーバとして動作させます。

プライベートサブネットにEC2 Auto Scalingグループを配置します。
こちらのインスタンスもAmazon Linux 2とします。
これらのインスタンスの働きは、Python(uWSGI)によって動作するアプリサーバです。

パブリックサブネットにNATゲートウェイを配置します。
先述のプライベートサブネット内に設置されているインスタンスの初期化処理時に、各種パッケージをインストールするために使用します。

Webサーバとアプリサーバ間にNLBを配置します。
NLBは内部用のものとして作成します。

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

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

CloudFormationテンプレートファイル

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

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

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

本ページは、内部NLBを使用して、Webサーバとアプリサーバを分離する方法を中心に取り上げます。

プライベートサブネット内のリソースをELB(ALB)にアタッチする方法については、以下のページをご確認ください。

スケーリングポリシーなしのAuto Scalingについては、以下のページをご確認ください。

NLB

Resources: NLB: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Name: !Sub "${Prefix}-NLB" Scheme: internal Subnets: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 Type: network NLBTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: Name: !Sub "${Prefix}-NLBTargetGroup" Protocol: TCP Port: !Ref UWSGIPort TargetGroupAttributes: - Key: preserve_client_ip.enabled Value: false VpcId: !Ref VPC HealthCheckProtocol: TCP HealthyThresholdCount: !Ref HealthyThresholdCount UnhealthyThresholdCount: !Ref UnhealthyThresholdCount HealthCheckIntervalSeconds: !Ref HealthCheckIntervalSeconds NLBListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - TargetGroupArn: !Ref NLBTargetGroup Type: forward LoadBalancerArn: !Ref NLB Port: !Ref UWSGIPort Protocol: TCP
Code language: YAML (yaml)

内部NLBを作成する上でのポイントはNLB本体のSchemaプロパティです。
本プロパティに「internal」を指定します。

Webサーバとアプリサーバ間の通信は9090/tcpで行います。
ですからターゲットグループおよびリスナーにおいて、本通信をリッスンし、ターゲットであるEC2インスタンスにルーティングするように設定します。

NLBのもう1つのポイントはクライアントIPの保持機能を無効化している点です。
本設定はTargetGroupAttributesプロパティで実施します。
無効化の目的は、NLBからルーティングされるパケットの送信元アドレスを、NLBのプライベートアドレスにするためです。
この設定の狙いは、アプリサーバであるEC2インスタンス用セキュリティグループに関係があります。
つまりセキュリティグループルールにおいて、許可する通信の送信元を、NLBのプライベートアドレスに設定することができるということです。
詳細につきましては、以下のページをご確認ください。

Webサーバ

EC2インスタンス

Resources: EIP1: Type: AWS::EC2::EIP Properties: Domain: vpc EIPAssociation1: Type: AWS::EC2::EIPAssociation Properties: AllocationId: !GetAtt EIP1.AllocationId InstanceId: !Ref Instance1 Instance1: Type: AWS::EC2::Instance Properties: IamInstanceProfile: !Ref InstanceProfile ImageId: !Ref ImageId InstanceType: !Ref InstanceType NetworkInterfaces: - DeviceIndex: 0 SubnetId: !Ref PublicSubnet1 GroupSet: - !Ref WebServerSecurityGroup Tags: - Key: !Ref InstanceTagKey Value: !Ref InstanceTagValueWeb1
Code language: YAML (yaml)

Webサーバとして動作する上で、特別な設定は不要です。
ただElastic IPアドレスを用意し、インスタンスに関連づけることによって、グローバルアドレスを固定化します。

以下の通り、タグを設定します。

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

セキュリティグループ

Resources: WebServerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${Prefix}-WebServerSecurityGroup" GroupDescription: Allow HTTP. 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)で行われます。
ですからこれを許可するようにルールを定義します。

SSM関連付け

Resources: RunShellScriptAssociation: 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 InstanceTagValueWeb1 WaitForSuccessTimeoutSeconds: !Ref WaitForSuccessTimeoutSeconds
Code language: YAML (yaml)

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

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

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

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

アプリサーバ

Auto Scalingグループ

Resources: LaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: LaunchTemplateData: IamInstanceProfile: Arn: !Ref InstanceProfileArn ImageId: !Ref ImageId InstanceType: !Ref InstanceType SecurityGroupIds: - !Ref AppServerSecurityGroup TagSpecifications: - ResourceType: instance Tags: - Key: !Ref InstanceTagKey Value: !Ref InstanceTagValueApp LaunchTemplateName: !Sub "${Prefix}-LaunchTemplate" AutoScalingGroup: Type: AWS::AutoScaling::AutoScalingGroup Properties: AutoScalingGroupName: !Sub "${Prefix}-AutoScalingGroup" DesiredCapacity: !Ref DesiredCapacity LaunchTemplate: LaunchTemplateId: !Ref LaunchTemplate Version: !GetAtt LaunchTemplate.LatestVersionNumber MaxSize: !Ref MaxSize MinSize: !Ref MinSize VPCZoneIdentifier: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 TargetGroupARNs: - !Ref NLBTargetGroup
Code language: YAML (yaml)

アプリサーバとして、Auto Scalingグループを構築します。
特別な設定は不要です。

以下の通り、タグを設定します。

  • キー:Server
  • 値:App

こちらもインスタンスの初期化処理において、SSMドキュメントを使用しますが、その際に、このタグ情報を使用します。

セキュリティグループ

Resources: AppServerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub "${Prefix}-AppServerSecurityGroup2" GroupDescription: Allow uWSGI from Web Server. VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: !Ref UWSGIPort ToPort: !Ref UWSGIPort CidrIp: !Sub "${NLBPrivateAddress1}/32" - IpProtocol: tcp FromPort: !Ref UWSGIPort ToPort: !Ref UWSGIPort CidrIp: !Sub "${NLBPrivateAddress2}/32"
Code language: YAML (yaml)

Webサーバからアプリサーバへの通信は、NLBを経由し、UNIX Domain Socket(9090/tcp)で行われます。
ですからこれを許可するようにルールを定義します。

今回は2つのルールを定義します。
これはNLBに割り当てられたプライベートアドレスが2つあるためです。
NLBが持つプライベートアドレスの数は、NLBに関連付いているサブネット数によって定まります。
今回の構成では、NLBに2つのサブネットに関連付けていますから、2つのアドレスを持つということになります。

NLBの項目で確認した通り、今回の構成では、クライアントIPの保持機能は無効化しています。
そのためNLBを経由後、パケットの送信元アドレスは、NLBのプライベートアドレスに置き換わります。

以上をまとめますと、セキュリティグループルールで許可するべき送信元アドレスは、NLBの2つのプライベートアドレスとなります。

SSM関連付け

Resources: ApplyAnsiblePlaybooksAssociation: 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: - SSM=True 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)

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

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 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: reload daemon. systemd: daemon_reload: yes - name: start and enable uWSGI. systemd: name: uwsgi state: started enabled: yes
Code language: YAML (yaml)

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

  • yumとpipでパッケージをインストールする。
  • Pythonスクリプトを配置する。
  • uWSGIファイルを配置する。
  • uWSGIサービス用ファイルを配置する。
  • uWSGIサービスを起動・有効化する。

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

Pythonスクリプト

import urllib.request from flask import Flask app = Flask(__name__) url = 'http://169.254.169.254/latest/meta-data/instance-id' @app.route('/') def main(): request = urllib.request.Request(url) with urllib.request.urlopen(request) as response: data = response.read().decode('utf-8') return data if __name__ == '__main__': app.run()
Code language: Python (python)

WebフレームワークFlaskで簡易的なアプリを作成します。
ルートURLにアクセスした場合、インスタンスIDを返します。

uwsgi.ini

[uwsgi] chdir = /home/ec2-user/myapp socket = 0.0.0.0:9090 file = /home/ec2-user/myapp/run.py callable = app master = True uid = ec2-user gid = ec2-user
Code language: plaintext (plaintext)

先述のPythonスクリプトを実行する内容です。

uwsgi.service

[Unit] Description = uWSGI After = syslog.target [Service] WorkingDirectory = /home/ec2-user/myapp/ 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)

uwsgi.iniを指定して、uWSGIを実行する内容です。

環境構築

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

Ansible Playbookの準備

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

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

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

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

  • NLB:fa-092-NLB
  • NLBのDNS名:ec2-18-176-243-76.ap-northeast-1.compute.amazonaws.com
  • NLBのターゲットグループ:fa-092-NLBTargetGroup
  • Webサーバインスタンス:i-036a057a2caa32486

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

Detail of NLB 1.

NLBのDNS名等が確認できます。

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

Detail of NLB 2.

ターゲットタブを見ると、2つのEC2インスタンスが登録されていることがわかります。
これらのEC2インスタンスがアプリサーバとして動作します。

WebサーバであるEC2インスタンスの詳細を確認します。

Detail of EC2 1.

NLBのDNS名がec2-18-176-243-76.ap-northeast-1.compute.amazonaws.comであることがわかります。

SSMドキュメントの実行状況を確認します。

Detail of SSM 1.

確かに2つのドキュメントが実行されていることがわかります。

まずAWS-RunShellScriptを実行し、Webサーバ用の初期化時の実行ログを確認します。

Loaded plugins: extras_suggestions, langpacks, priorities, update-motd ... ================================================================================ Package Arch Version Repository Size ================================================================================ Installing: httpd aarch64 2.4.54-1.amzn2 amzn2-core 1.4 M ... Installed: httpd.aarch64 0:2.4.54-1.amzn2 ... Complete!
Code language: plaintext (plaintext)

確かにApacheがインストールされていることがわかります。

続いてAWS-ApplyAnsiblePlaybooksを実行し、アプリサーバ用の初期化時の実行ログを確認します。

Amazon Linux release 2 (Karoo) ... PLAY [all] ********************************************************************* TASK [update yum.] ************************************************************* ok: [localhost] => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}, "changed": false, "msg": "", "rc": 0, "results": ["libjpeg-turbo-2.0.90-2.amzn2.0.1.aarch64 providing * is already installed"]} TASK [install packages by yum.] ************************************************ changed: [localhost] => {"ansible_facts": {"pkg_mgr": "yum"}, "changed": true, "changes": {"installed": ["python3-devel", "gcc"]}, "msg": "", "rc": 0, ...} TASK [install packages by pip3.] *********************************************** changed: [localhost] => {"changed": true, "cmd": ["/usr/bin/pip3", "install", "uwsgi", "flask"], "name": ["uwsgi", "flask"], ...} TASK [create app directory.] *************************************************** changed: [localhost] => {"changed": true, "gid": 0, "group": "root", "mode": "0755", "owner": "root", "path": "/home/ec2-user/myapp", "size": 6, "state": "directory", "uid": 0} TASK [copy flask script.] ****************************************************** changed: [localhost] => {"changed": true, "checksum": "392c86110eaf72f4ba78c5462ebfe5ea4bddc962", "dest": "/home/ec2-user/myapp/run.py", "gid": 0, "group": "root", "md5sum": "c7db1fa4405b84afe32795fbad73abc6", "mode": "0644", "owner": "root", "size": 352, "src": "/root/.ansible/tmp/ansible-tmp-1666527040.07-114179067830016/source", "state": "file", "uid": 0} TASK [copy uWSGI ini.] ********************************************************* changed: [localhost] => {"changed": true, "checksum": "dc25dc7fbba4d1dc3f78f765cf65cf5e6af9672b", "dest": "/home/ec2-user/myapp/uwsgi.ini", "gid": 0, "group": "root", "md5sum": "e20220d2bcd7012cc2136339322f54b4", "mode": "0644", "owner": "root", "size": 171, "src": "/root/.ansible/tmp/ansible-tmp-1666527040.66-271438457070822/source", "state": "file", "uid": 0} TASK [copy uWSGI Service.] ***************************************************** changed: [localhost] => {"changed": true, "checksum": "5f1a6f6e4469de744b10fe8de37af7847e33c644", "dest": "/etc/systemd/system/uwsgi.service", "gid": 0, "group": "root", "md5sum": "c520ed8cb1af45762b3d2d51c08eae8f", "mode": "0644", "owner": "root", "size": 406, "src": "/root/.ansible/tmp/ansible-tmp-1666527040.9-17771148645236/source", "state": "file", "uid": 0} TASK [reload daemon.] ********************************************************** ok: [localhost] => {"changed": false, "name": null, "status": {}} TASK [start and enable uWSGI.] ************************************************* changed: [localhost] => {"changed": true, "enabled": true, "name": "uwsgi", "state": "started", ...}} PLAY RECAP ********************************************************************* localhost : ok=9 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Code language: plaintext (plaintext)

確かに各種パッケージがインストールされ、uWSGIが動作していることがわかります。

動作確認

準備が整いましたので、WebサーバであるEC2インスタンスにアクセスします。

Detail of EC2 2.
Detail of EC2 3.

Webサーバにアクセスすることで、NLBを経由し、Auto Scalingグループ内の2つのEC2インスタンスにアクセスできました。
以上のことから、内部NLBを配置することで、Webサーバとアプリサーバを分離することができました。

まとめ

Webサーバとアプリサーバを分離し、両サーバの間に内部NLBを配置して、両者を連携させる方法を確認しました。

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