본문 바로가기

Public Cloud/AWS

AWS 자습 노트 - 8. Application Load Balancer with Spring Boot

반응형

자습 시나리오

AWS의 Load Balancer를 활용하기 위해 다음과 같은 시나리오를 가정하고 실습해 보았다 

  1. Spring Boot 기반으로 2개의 Web App 개발
    1. sampleweb : 설치된 Machine의 hostname과 IP 정보 제공
    2. secondsampleweb : "Hello." + 설치된 Machine의 hostname 제공
  2. Build 후에 생성된 Jar 파일을 S3로 Upload
  3. EC2 Instance 3개를 각각 다른 AZ에서 생성 후 S3에서 sampleweb jar 파일을 가져와 Launching
  4. EC2 Instance 2개를 각각 다른 AZ에서 생성 후 S3에서 secondsampleweb jar 파일을 가져와 Launching
  5. AWS Application Load Balancer를 생성 후 root path로 접속 시에는 sampleweb으로 연결, '/hello' path로 접속 시에 secondsampleweb으로 연결
  6. Root Path로 연속 Request 전달 시 3번에서 생성한 3개의 EC2로 Load가 분산되는 지 Test
  7. '/hello' path로 접속 시 4번에서 생성한 2개의 EC2로 Load가 분산되는 지 Test

Test Code

이 실습에서 사용할 Test WEB Application인 samplewebsecondsampleweb Application 개발 환경은 다음과 같다.

  • JDK 1.8 
  • Spring Boot 2.1.4
  • Gradle 5.4

각 Project의 명칭은 samplewebsecondsampleweb이고 간단한 Controller Code와 HTML Page로 구성하였다. 

 

[sampleweb project]

sampleweb application에서는 Root Path로 GET request가 왔을 때 application이 실행되는 기기의 명칭과 ip를 template에 전달하고, 이를 보여주는 HTML page가 Return 된다.

 

Controller code

...
@Controller
@RequestMapping("/")
public class SampleController {

    @GetMapping("/")
    public String getUser(Model model) {
    	
    	String hostName, ipaddress;
    	
    	try {
    		hostName = InetAddress.getLocalHost().getHostName();
    		ipaddress = InetAddress.getLocalHost().getHostAddress();
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			hostName = new StringBuilder("Error : ").append(e.getLocalizedMessage()).toString();
			ipaddress = "";
		}
    	
    	model.addAttribute("hostname", hostName);
    	model.addAttribute("ipaddress", ipaddress);
    	
        return "index";
    }
	
}
...

 

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="EUC-KR">
<title>Insert title here</title>
</head>
<body>
	<H1 th:text="'Welcome to ' + ${hostname}"></H1>
	<h3 th:text="'This IP is ' + ${ipaddress}"></h3>
</body>
</html>

 

[secondsampleweb project]

secondsampleweb의 controller와 HTML도 거의 동일한 Code로 되어 있고 차이를 확인하기 위해 약간의 변화만 주었다. Controller에서는 접근 경로만 변경했고, HTML에서는 'Hello'라는 단어를 추가했다.

 

Controller code

...
	@GetMapping("/hello")
    public String getUser(Model model) {
    	
	...
    
    }
...

 

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="EUC-KR">
<title>Insert title here</title>
</head>
<body>
	<H1 th:text="'Hello, ' + ${hostname}"></H1>
</body>
</html>

Compile & 실행 파일 Upload

EC2 Instance에서 이 프로젝트를 실행할 수 있게 gradle을 이용해 build 한 후에 생성된 jar file들을 여러 EC2 Instance에서 쉽게 가져갈 수 있게 AWS Console을 통해 S3에 Upload 하였다(<pic 1> 참조). 여기서 사용된 S3 버킷은 'browndwarfautoscalingtest'이라는 이름으로 새로 생성했으며, 나머지 설정은 기본값 그대로 두었다.

...
[browndwarf@localhost sampleweb]$ ./gradlew clean build
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :test
2019-07-28 00:08:23.445  INFO 28385 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

BUILD SUCCESSFUL in 36s
6 actionable tasks: 6 executed
...
[browndwarf@localhost sampleweb]$ ls -la ./build/libs/
total 18612
drwxrwxr-x. 2 dohoon dohoon       31 7월 28 00:08 .
drwxrwxr-x. 9 dohoon dohoon     4096 7월 28 00:08 ..
-rw-rw-r--. 1 dohoon dohoon 19054335 7월 28 00:08 sampleweb-1.0.jar

...

 

<pic 1. Jar 파일 Upload 된 모습>

EC2 생성, 환경 설정 및 Service Launching

일반적으로 Load Balancer를 구성할 때에는 EC2 Instance들을 두 개 이상의 AZ에 걸쳐서 배치하는 것을 권장하기 때문에, EC2가 사용할 Subnet들을 각각 다른 AZ에 위치하게 생성하였고 (서브넷은 AWS Console > VPC > 서브넷으로 진입해서 생성하면 된다) 각 Subnet의 이름과 IP 정보, AZ 정보는 아래와 같다.

 

  • az1-subnet : 172.31.192.0/18, ap-northeast-2a
  • az2-subnet : 172.31.128.0/18, ap-northeast-2b
  • az3-subnet : 172.31.64.0/18, ap-northeast-2c

EC2 Instance를 생성해서 S3에 있는 jar 파일을 Download하려면 AWS Cli의 설치가 필요한데, Amazon Linux AMI를 사용할 경우에는 별도의 설치가 필요없다. (여기서 사용한 AMI 이름은 Amazon Linux 2 AMI (HVM), SSD Volume Type 이다)

 

s3에서 파일을 upload/downloaf 하는 AWS Cli 명령은 다음과 같다

aws s3 cp [source url] [target url]

ex) s3에서 Local로 download
aws s3 cp s3://browndwarfautoscalingtest/sampleweb-1.0.jar ./

ex) Local에서 S3로 upload
aws s3 cp ./sampleweb-1.0.jar s3://browndwarfautoscalingtest/

하지만 생성한 EC2 Instance에 SSH로 접속해서 위의 명령을 실행하면 아래와 같이 인증 오류가 발생한다.

...
[ec2-user@ip-172-31-224-156 ~]$ aws s3 cp s3://browndwarfautoscalingtest/sampleweb-1.0.jar ./
Unable to locate credentials. You can configure credentials by running "aws configure".
...

이 오류는 명령을 실행한 EC2 Instance에 S3에 있는 객체를 읽어올 권한이 없기 때문에 발생하는 문제이다. EC2 Instance에서 S3에 접근할 수 있는 권한을 부여하려면, AWS Cli가 제공하는 'aws configure' command를 통해 인증 키를 등록하면 되지만, 이 방법은 보안 관점에서 피해야할 방법이다. 대신 IAM을 통해 EC2 Instance를 위한 Role을 생성한 후에 이를 EC2에 부여하는 것이 권장되고 있다.

 

IAM에서 EC2 Instance를 위한 Role 생성

AWS Portal > IAM > 역할 > 역할 만들기로 진입하면 <pic 2> 화면이 나온다.  1단계에서는 생성할 Role을 어디에 부여하는 지를 선택하게 되는데 AWS 서비스의 EC2를 선택하면 된다.

 

<pic 2. Role 생성 화면 1단계>

다음 단계에서는 <pic 3>와 같이 생성하려는 Role에 부여할 권한(Permission)을 정의한다. 각 권한들은 권한 정책(Policy)이라는 논리적인 조합으로 묶여서 Role에 부여되는데, 권한 정책을 새로 생성할 필요는 없고 이미 만들어진 정책 중에 'AmazonS3FullAccess' 정책을 선택하면 된다.

<pic 3. Permission Policy 연결>

3단계 Tag 설정 단계를 건너 뛰고 <pic 4>와 같은 4단계 화면에서 Role의 이름을 부여하고 나머지 설정 사항을 검토한 후 '역할만들기' 버튼을 클릭하면 Role 생성이 끝난다.

<pic 4. Role 생성 검토 화면>

 다시 AWS Console > EC2 > 인스턴스로 돌아와 생성한 EC2 Instance를 선택한 후에 작업 > 인스턴스 설정 > IAM 역할 연결/바꾸기를 선택하면 <pic 5> 화면으로 진입하게 된다. 여기서 생성했던 IAM role을 선택하고  적용하면 된다.

<Pic 5. EC2 Instance의 IAM Role 설정 변경> 

위 설정을 끝내면 AWS Cli 명령을 바로 실행해면 jar 파일을 S3 Bucket에서 문제없이 Download 해온다.

...
[ec2-user@ip-172-31-224-156 ~]$ aws s3 cp s3://browndwarfautoscalingtest/sampleweb-1.0.jar ./
download: s3://browndwarfautoscalingtest/sampleweb-1.0.jar to ./sampleweb-1.0.jar
...

 

이 설정은 EC2 Instance를 만드는 과정에서 바로 적용할 수 있다. EC2 Insance 생성 과정 중 <pic 6>과 같은 '인스턴스 구성' 단계에서 중간에 위치한 IAM 역할 Combo box에서 IAM Role을 선택하면 된다.

<pic 6. EC2 Insance 생성 과정에서 IAM 설정>

그리고 같은 화면에 하단에 user data입력하는 <pic 7>과 같은 Text Box가 있다. 여기에 초기 환경 구성을 위한 아래의 Shell Script를 넣으면 생성한 EC2 Instance가 바로 sampleweb service를 Lauching 할 수 있다.

<pic 7. EC2 Instance 생성시 실행할 Script 입력>

 

EC2 Instance Initializing Script

 
#!/bin/bash
sudo yum update -y
sudo yum remove java-1.7.0-openjdk -y
sudo yum install java-1.8.0-openjdk -y
aws s3 cp s3://browndwarfautoscalingtest/sampleweb-1.0.jar .
nohup java -jar sampleweb-1.0.jar &
 

위와 같은 과정을 통해 2개의 AZ에 걸쳐 sampleweb service가 Launching된 3개의 EC2 Instance를 만들었다. (ap-northeast-2b AZ에서는 t2.micro insance가 만들어지지 않아 ap-northeast-2a에 2개의 Instance를 만들었다.)

이름 가용영역 Public IP Private IP Subnet
SampleWebinAZ1 ap-northeast-2a 13.124.155.181 172.31.224.156 az1-subnet
SampleWebinAZ1-2 ap-northeast-2a 13.124.80.244 172.31.213.34 az1-subnet
SampleWebinAZ3 ap-northeast-2c 13.125.76.206 172.31.73.93 az3-subnet

<table-1. sampleweb EC2 Instances>

 

3개의 EC2 Instance는 모두 Public IP를 가지고 있고 보안그룹에서 8080 port를 public에 Open 하였기 때문에 모두 브라우져를 통한 접근이 가능하다. 그 결과가 <pic 8>, <pic 9>, <pic 10>과 같다. 모두 자신의 Private IP에 기초한 정보를 정상적으로 보여주고 있다.

<pic 8. SampleWebinAZ1 접속 화면>
<pic 9. SampleWebinAZ1-2 접속 화면>
<pic 10. SampleWebinAZ3 접속 화면>

그리고 /hello path로 접근해야 하는 secondsampleweb을 실행시킬 2개의 Instance를 생성하였다. 거의 동일한 과정으로 진행했는데, user data로 삽입한 script만 약간 변경하였다.

 
#!/bin/bash
sudo yum update -y
sudo yum remove java-1.7.0-openjdk
sudo yum install java-1.8.0-openjdk -y
aws s3 cp s3://browndwarfautoscalingtest/secondsampleweb-1.0.jar .
nohup java -jar secondsampleweb-1.0.jar &
 

생성한 secondsampleweb EC2 Instance는 다음과 같다.

이름 가용영역 Public IP Private IP Subnet
SecondSampleWebinAZ1 ap-northeast-2a 52.79.129.29 172.31.233.74 az1-subnet
SecondSampleWebinAZ3 ap-northeast-2c 52.79.253.22 172.31.98.46 az3-subnet

<table-2. secondsampleweb EC2 Instances>

위와 마찬가지로 Browser를 통해 접근했을 때 모두 자신의 Private IP를 기반으로 <pic 11>, <pic 12>와 같이 정상적인 결과를 보내주고 있다.

 

<pic 11. SecondSampleWebinAZ1 접속 화면>

 

<pic 12. SecondSampleWebinAZ3 접속 화면>

 

Target Group 생성 및 설정

<pic 13. Load Balancer의 Routing 설계>

이 자습에서는 단순히 Request Traffic을 분산하는 것 외에, <pic 13>과 같이 URL Path를 기반으로한 Path based Routing 기능 까지 Load Balancer를 통해 구성하려는 것이 목표였기 때문에 AWS의 Application Load Balancer(이하 ALB)를 사용해야 한다.(Classic Load Balancer는 이 기능을 제공하지 않는다.)

URL Path에 따라 Request의 전달 대상들을 다르게 하려면 같은 용도의 EC2 Instance들을 Target Group으로 Grouping 해주는 것이 좋다. AWS Portal > EC2에 진입해서 좌측 메뉴 중 '대상 그룹'을 선택한 후 '대상 그룹 생성'버튼을 클릭하면 <pic 14>로 진입 하게 된다. 다음 값을 입력해 Target Group을 생성한다.

 

  • 이름 : sampleweb-group(Target Group을 가르키는 이름)
  • Port : 8080 (EC2 Instance들이 Listening 하는 port)
  • VPC : vpc-45fd162e (EC2 Instance들이 속한 VPC 명)
  • 경로 : / (root 경로)

<pic 14. Target Group 생성 화면>

생성이 끝나면 <pic 15>에서 처럼 대상 그룹 목록을 확인할 수 있고, 하단의 Tab들 중에 '대상' Tab으로 가서 '편집'버튼을 클릭하면 대상그룹에 속할  EC2 Instance를 편집하는 <pic 16>으로 진입하게 된다.

 

<pic 15. Target Group List 및 정보 화면>

 

<pic 16>에서 제일 하단의 EC2 Instance List에서 SampleWebInAZ1, SampleWebInAZ1-2, SampleWebInAZ3 등 3개를 선택해 '등록된 항목에 추가' 버튼을 누르면 Target Group에 EC2 Instance들이 포함되게 된다.

<pic 16. Target Group 편집 화면>

 

여기까지가 sampleweb project를 실행하는 EC2 Instance들의 Group인 sampleweb-group 생성 및 설정 과정이고, 동일한 과정을 통해 secondsampleweb project를 위한 secondweb-group도 생성한다. secondweb-group의 등록 정보는 다음과 같고, <pic 17>, <pic 18>이 생성 및 설정 과정이다.

 

  • 이름 : secondweb-group
  • Port : 8080 (이전과 동일)
  • VPC : vpc-45fd162e (이전과 동일)
  • 경로 : /hello

<pic 17.  secondweb-group 생성 화면>
<pic 18. secondweb-group에 속할 EC2 Instance 편집 화면>

 

Load Balancer 생성 및 설정

Target Group들을 다 생성한 후에 Load Balance를 생성한다. AWS Portal > EC2로 진입한 후 좌측 메뉴에서 로드밸러서를 선택하면 <pic 19>와 같은 화면이 나온다. AWS의 Load Balancer에는 Application Load Balancer(이하 ALB), Network Load Balancer(이하 NLB), Classic Load Balancer 3종류가 있고, Web Service에 대한 Load Balance는 ALB나 Classic Load Balancer로 구성하면 된다. 다만, Classic Load Balancer는 Path Based Routing을 지원하지 않아 앞에서 언급한 대로 ALB를 사용하였다.

 

<pic 19. AWS의 Load Balancer 종류>

<pic 20>의 ALB 생성 1단계에서는 Load Balancer 이름과 Listening Port를 입력한다. 그리고 <pic 21>과 같이 1단계 하단에 있는 Load Balancer이 적용될 가용 영역(AZ)에 ap-northeast-2a, 2b, 2c를 모두 포함시켰다. (단, 이 예제에서는 ap-northeast-2b에 포함된 EC2 Instance는 없다.)

<pic 20. 생성할 로드밸런서 설정 화면>

 

<pic 21. 로드밸러서가 적용될 가용영역 설정 화면>

3단계는 Security Group을 설정하는 부분이다. <pic 22>와 같이 일반 Browser로 접근할 수 있게 80 port를 Open 하였다.

<pic 22. 로드 밸런서의 보안 그룹 설정 화면>

<pic 23>의 4단계에서 Request Routing 대상을 설정한다. 여기서 위에서 생성한 Target Group을 선택하면 되는데, sampleweb-group을 먼저 사용했다. (대상 그룹이 정해지면 Port와 경로가 자동으로 등록되게 된다.)

<pic 23. 로드밸런서의 라우팅 구성 화면>

라우팅 구성 과정에서 대상 그룹을 입력했기 때문에 대상 등록 단계에서는 <pic 24>와 같이 EC2 Instance를 확인만 하고 ALB를 생성하면 된다. 

<pic 24. 로드 밸런서의 대상 등록 화면>

만들어진 로드밸런스는 <pic 25>와 같이 확인할 수 있다. 그리고 하단부에 있는 기본 구성 사항 중에 'DNS 이름' 에 있는 URL을 통해 Browser로 접근할 수 있다. 현재는 root path로 접근할 때 sampleweb-group에 속한 EC2 Instance만 접근할 수 있고, /hello path로 Request가 전달될 때의 secondweb-group으로 전달하려면 Routing 규칙을 추가해야 한다. 

<pic 25. 로드 밸런서 목록 및 구성 정보 화면>

Routing 규칙을 추가하려면 <pic 25>의 하단에 있는 여러 Tab 중에서 '리스너' tab을 연다. 리스너 Tab은 <pic 26>과 같으며 현재는 80 로 접근하는 Request가 sampleweb-group으로 Routing 되는 것을 확인할 수 있다. 그리고 그 아래에서 '규칙 보기/편집'을 클릭해서 규칙을 추가할 수 있다.

<pic 26. 로드 밸런서의 리스너 Tab>

규칙 설정 화면은 <pic 27> 과 같다. 이전에 만든 기본 규칙 외에 Routing을 추가하려면 상단에 '+' 버튼을 클릭한다.

<pic 27. Routing Rule List 화면>

'+' 버튼을 클릭하면 <pic 28>과 같이 Routing 규칙을 추가하기 위한 IF-THEN 편집 부분을 확인할 수 있다. IF에는 '호스트 헤더', '경로', 'HTTP 헤더', 'HTTP 요청 메서드', '쿼리 문자열', '소스 IP' 등을 선택할 수 있고, THEN에서는 '전달 대상', '리디렉션 대상', '고정 응답 반환' 등으로 Routing을 설정할 수 있다.

<pic 28. Routing Rule 편집>

여기서는 경로가 '/hello'로 접근하는 Request에 대해 'SecondWeb-Group'에 속한 EC2 Instance로 Request를 Routing하려고 했기 때문에 <pic 29>와 같이 설정을 추가하였고, '저장' 버튼을 누르면  <pic 30>과 같이 된다.

<pic 29. 추가할 Routing Rule 편집 결과>
<pic 30. Routing Rule 최종 상태>

Load Balancer 동작 결과

이제 모든 과정이 끝났다. ALB의 DNS(alb1-1725724230.ap-northeast-2.elb.amazonaws.com)로 접근하면 <pic 31>, <pic 32>, <pic 33>와 같이 sampleweb-group에 속한 EC2 Instance로 Request가 분산되는 것을 확인할 수 있다.

<pic 31. SampleWebinAZ1로 Request가 전달된 경우>

 

<pic 32. SampleWebinAZ1-2로 Request가 전달된 경우>
<pic 33. SampleWebinAZ3로 Request가 전달된 경우>

 마찬가지로 /hello path를 추가하면 <pic 34>, <pic 35>와 같이 secondweb-group에 속한 EC2 Instance로 Request가 분산된다.

<pic 12. SecondSampleWebinAZ1로 Request가 전달된 경우>
<pic 12. SecondSampleWebinAZ3로 Request가 전달된 경우>

 

Request 분산 결과

이제 Request 들이 단시간에 몰릴 때에 Load 분산이 어떻게 되는 지 확인하기 위해 아래와 같이 두 개의 Shell Script를 준비했다. 각각 Host와 호출 횟수를 인자로 해서 실행하면 Return으로 오는 HTML page 중에서 IP 정보를 추출해서 파일에 남기게 하는 Script 이다.

...
[browndwarf@localhost Public]$ cat curlloop.sh
#!/bin/bash
for((i=1;i <= $2;i++))
do
	curl  --silent $1 2>&1 | grep 'This IP is' | sed -e 's/<[^>]*>//g' | sed -e 's/This IP is //'
done
...

<sampleweb-group을 위한 script>

 

...
[browndwarf@localhost Public]$ cat curlloopsecond.sh
#!/bin/bash
for((i=1;i <= $2;i++))
do
	curl  --silent $1/hello 2>&1 | grep 'Hello' | sed -e 's/<[^>]*>//g' | sed -e 's/Hello, ip-\(.*\).ap.*/\1/'

done
...

<secondsampleweb-group를 위한 script>

 

위 스크립트를 이용해 각각 100번씩 호출하게끔 하였다.

...
[browndwarf@localhost Public]$ ./curlloop.sh http://alb1-1725724230.ap-northeast-2.elb.amazonaws.com 100 > callresult.txt
[browndwarf@localhost Public]$ ./curlloopsecond.sh http://alb1-1725724230.ap-northeast-2.elb.amazonaws.com 100 > secondcallresult.txt
...

결과 파일을 분석했더니 sampleWeb-group에 속한 Instance들은 각각 33, 33, 34회, secondweb-group에 속한 Instance들은 49, 51회씩 호출되었다. 마치 자로 잰 듯한 결과였다.