内部NLBを使用して、Web・アプリサーバを分離する
ApacheとPythonでWebサービスを作成します。
Webサーバとアプリサーバを分離し、両サーバの間に内部NLBを配置することによって、両者を連携する方法を確認します。
構築する環境
パブリックサブネットに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テンプレートを配置しています。
https://github.com/awstut-an-r/awstut-fa/tree/main/092
テンプレートファイルのポイント解説
本ページは、内部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を確認します。
NLBのDNS名等が確認できます。
ターゲットグループを確認します。
ターゲットタブを見ると、2つのEC2インスタンスが登録されていることがわかります。
これらのEC2インスタンスがアプリサーバとして動作します。
WebサーバであるEC2インスタンスの詳細を確認します。
NLBのDNS名がec2-18-176-243-76.ap-northeast-1.compute.amazonaws.comであることがわかります。
SSMドキュメントの実行状況を確認します。
確かに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インスタンスにアクセスします。
Webサーバにアクセスすることで、NLBを経由し、Auto Scalingグループ内の2つのEC2インスタンスにアクセスできました。
以上のことから、内部NLBを配置することで、Webサーバとアプリサーバを分離することができました。
まとめ
Webサーバとアプリサーバを分離し、両サーバの間に内部NLBを配置して、両者を連携させる方法を確認しました。