요즘 프로젝트를 하고 있는데, 역시나 자바 시스템이 있다. Spring Boot3 을 사용하고 있고 자바 17을 쓰는등 나름대로 괜찮은 환경에서 개발이 이루어지고 있다. 그런데, 이것을 서버에서 배포를 하고 Spring Boot 를 실행해야 하는데, 어떻게 하나 봤더니 초보자 수준도 못 벗어나는 설정을 하고 있으니… 안타까운 마음에 어떻게 하는 것이 좋은 것인지 한번 적어봤다.
Spring boot, jar 실행 파일
Spring Boot3 를 컴파일 하면 jar 파일 나온다. 그리고 별다른 서버 없이도 바로 실행하고 접속이 가능해 진다. 한가지 재미있는 사실은 많은 사람들이 Spring boot3 에 실행 파일 jar 이 내장된 WAS 서버가 구동되면서 실행된다는 걸 모른다는 거다. 심지여 그것이 Tomcat 이라는 것도. 물론 어떤 프로그래밍 모델인지에 따라서 Tomcat 이 되기도 하고 Netty 되기도 하지만, 과연 Reactive Programming Model 로 짜는 사람이 몇이나 있나 싶다. 대부분 Spring MVC 이고 Servlet 이면 기본적으로 Tomcat 이 구동된다. 내장형 Tomcat
CloudWatch Log 의 총 용량의 사용량을 알아야 얼마 정도 비용이 나가는지를 추정할 수 있다. CloudWatch Log 화면에서 총 용량을 보여주면 고맙겠지만 그런게 없다보니 비용이 얼마정도 나가는지도 모르게된다.
Python 을 이용해서 총 용량을 계산해보자.
nextToken
AWS SDK 를 사용하는데 있어 항상 나오는 것이 nextToken 이다. AWS 는 무제한으로 API Call 을 하도록 허용하지도 않고 결과의 모두를 출력해주지 않는다. 다음을 보자.
limit 제한
Python
1
2
client=session.client('logs')
response=client.describe_log_groups(limit=50)
describe_log_groups 메소드에 인자값으로 limit 가 있다. 이 값은 Default 값이 있다. 문제는 이 인자값을 50 이상을 지정할 수가 없다는 것이다. CloudWatch Log 개수가 50개 이상이라면 딱 50개 만 출력이되는데, 나머지를 출력하고 싶다면 nextToken 인자를 주면 된다.
node_exporter 디렉토리가 보이고 그 안에 node_exporter 바이너파일이 있다. 이것을 적당한 곳으로 이동시키놓으면 끝난다.
어떻게 시작/중지 할건지…
여기서 이제 고민을 해야한다. 많은 사람들은 이것을 쉘 스크립트와 nohup 을 사용하는 사람들이 있다. 어느 시대에 살고 있는 사람들인지 의심이 될 정도인데, 이제는 init script 도 다 없어질 만큼 대부분의 배포판들이 systemd 로 다 전환이 완료된 상태다.
그러면 당연히 systemd 로 하면 되지! 하지만 여기서 문제가 된다.
현재 systemd 는 지속적으로 지금도 버전업이 되고 있다. 그러다보니 특정 버전을 기준으로 특정 기능이 지원이되고 안되고가 갈리게 된다.
systemd 버전 240 …. (대체 어떻게 버전 관리를.. 어떻게 기능을 많이 집어넣었으면 버전이 240 이여.. -_-;; ) 왠만하면 systemd 버전 240 이상을 사용할 것을 권한다. 그런데, 이게 말처럼,, 240버전을 써라~~ 한다고 되는게 아니다.
systemd 는 리눅스 시스템의 뼈대라고 보면된다. 핵심중에 핵심! 그러다보니 systemd 는 배포판과 함께 제공되고 묶여 있다. 240버전을 쓰고 싶다면 240버전을 가진 배포판을 써야 한다는 뜻이 된다.
Ubuntu 22.04: 249.11-0ubuntu3.12
RHEL 8.10: 239-82.el8_10.1
이런 저런 사유로 배포판을 선택하고 거기다 버전을 선택하게 된다. 내가 하고 있는 프로젝트에서는 CentOS, RHEL 7.9 가 대부분이고 RHEL 8 은 최신형으로 취급하는데.. systemd 만 놓고 보면 RHEL 8 도 그다지 마음에 들지 않는 부분이다.
개인적으로 RHEL 8 도 이제는 끝물이다. RHEL 9 의 버전이 이제는 벌써 9.3을 벗어나고 있기 때문에 이제는 RHEL 9 로가야 한다.
아무튼, 말이 길었는데, systemd 유닛으로 만들어 보자..
일단, node_exporter 에는 많은 옵션들이 있다. Prometheus exporter 들이 많은 옵션들을 제공한다. 이러한 옵션들은 별도의 파일로 작성하고 쉘 변수로 만들어 두고 systemd 유닛에서 읽어들이도록 하면 된다.
/etc/sysconfig/node_exporter 는 RedHat 기반에 적합하다. Ubuntu 면 /etc/sysconfig 디렉토리가 없기 때문에 Ubuntu 레이아웃에 맞는 곳에 넣으면 된다.
systemd 유닛 파일은 별거 없다.
systemd 유닛 파일
INI
1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=NodeExporter
Wants=network-online.target
After=network-online.target
[Service]
EnvironmentFile=/etc/sysconfig/node_exporter
ExecStart=/usr/local/bin/node_exporter$OPTIONS
[Install]
WantedBy=multi-user.target
딱 보면 별거 없다….. 하지만,,, 240 이하에서는 출력되는 로그들… 일명 Standard Out 들을 어떻게 처리할까? 그냥 이대로 둬도 되나? 결론은 되긴 한다. 이렇게 그대로 두면 node_exporter 가 뭔가를 출력하면 stdout, stderr 로 내보낸다. 그러면 systemd 는 이것을 /var/log/syslog 파일에 기록하게 된다.
하지만, systemd 로 변경되면서 나온 journal 에 기록을 하고 싶을지도 모른다. 이왕이면 그렇게하는게 좋기도 하다. 그래서 다음과 같이 [Service] 세션에 StandardOutput 옵션을 준다.
systemd log 관리
INI
1
StandardOutput=journal+console
콘솔에도 출력을…. 뭐.. 이건 옵션이다. 여기서 systemd 버전에 따라서 파일에 redirect 가 가능하기도 하고 불가능하기도 하다. StandardOutput 에 옵션이 inherit, null, tty, journal, kmsg, journal+console, kmsg+console, file:path, append:path, truncate:path 이렇게 되어 있다. 이게 다 가능한게 아니다. 버전에 따라서 가능하기도 하고 불가능하기도 하다.
Systemd 버전
새로운 배포판에 따라서 systemd 의 버전이 달라진다. 문제는 대부분 systemd 버전이 특정 시점까지만 업데이트가 된다. 시스템에 뼈대이다 보니 확 갈아 엎을 수 없는 것이여서 그럴거다.
이러다보니 특정 기능을 탐이 나는 때가 있는데, OS 를 다 갈아 엎어야하는 고충이 있다. 그래서 이왕이면 새로운 시스템을 구축할때에는 왠만하면 최신판을 쓰는게 좋다. Ubuntu 라면 24.04, RedHat 이면 9.0 을 사용하길 권한다.
2. Nginx 는 X-Forwarded-For 헤더에서 LB IP(b.b.b.b) 를 생략하고 Client 에 Real IP를 찾는데, $remote_addr 은 b.b.b.b 에서 a.a.a.a 로 변경되고 따라서 proxy_set_header X-Real-IP $remote_addr 은 맞는 설정이다. (내가 원하는 설정대로다.)
그런데, Nginx 는 X-Forwarded-For 헤더를 b.b.b.b 대신에 a.a.a.a IP 로 바꾼다.
3. WEBAPP 은 다음과 같은 헤더를 수신한다.
INI
1
2
3
X-Forwarded-For=a.a.a.a,a.a.a.a
X-Real-IP=a.a.a.a
->X-Forwarded-For는a.a.a.a,b.b.b.b여야한다.
내가 필요한 것은 먼저 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_forwarded_for를 설정한 다음 실제 IP를 검색하고 $remote_addr 값을 바꾸는 기능입니다.
이 문제를 어떻게 해결해야 하는지 알려줄 사람?
답1
$proxy_add_x_forwarded_for 는 $http_x_forwarded_for,$remote_addr 와 동일하고 $remote_addr 값은 http_realip_module 을 사용될때에 변경됩니다. 그래서 당신은 그 헤더에서 마지막 Proxy 주소를 얻을 수 없습니다. nginx 설정은 선언적이기 때문에 지시어의 순서 변경은 영향을 주지 않습니다.
http_realip_module 를 사용하면, $realip_remote_addr 값은 (nginx >= 1.9.7) 오리지널 $remote_addr 처럼 사용될 수 있습니다. (역, $realip_remote_addr 은 Nginx 앞에 있는 서버에 IP, 여기서는 LB(b.b.b.b) 를 가리킨다) 따라서 다음과 같이 X-Forwarded-For 를 세팅할 수 있습니다.
나도 같은 문제로 여기까지 왔는데, 이러한 현상은 real_ip_recursive on; 으로 발생된다는 결론을 내렸습니다. Nginx 의 공식문서에 다음과 같이 나옵니다.
If recursive search is enabled, an original client address that matches one of the trusted addresses is replaced by the last non-trusted address sent in the request header field.
이렇게 해서 돌려보면 ip 에는 Nginx 의 ip를 가지고 오게 된다. 이를 해결하기 위한 다양한 방법이 존재한다.
Nginx 설정
Nginx 에서 Real IP 를 가지고 오기 위해서는 Nginx 에 자체 변수인 $remote_addr 에 Client IP 를 넣어줘야 한다. 여기서 ‘$remote_addr 변수에 값을 넣어줘야’ 말이 중요하다.
Nginx 에서도 앞에 수많은 Proxy 가 있을 경우에 $remote_addr 은 이전 Proxy 의 IP 주소가 된다.
Client —-> AWS ALB ——> Nginx
만일 위와같은 구조일 경우에 Nginx 에 $remote_addr 값은 AWS ALB 의 값이 된다.
이 문제를 해결하기 위해서 Nginx 에 ‘–with-http_realip_module’ 을 가지고 있을 경우에 특정 헤더에서 Real Ip 를 찾을 수 있도록 해준다. AWS ALB 의 경우에는 X-Forwarded-For 헤더에 Client IP 를 보존해 준다. 따라서 Nginx 에서는 다음과 같이 설정해줌으로써 $remote_addr 에 Client IP를 넣을 수 있다.
realip config
Apache
1
2
3
4
set_real_ip_from172.10.10.0/24;# ALB A zone
set_real_ip_from172.10.20.0/24;# ALB C zone
real_ip_headerX-Forwarded-For;
real_ip_recursiveon;
X-Forwarded-For 라는 헤더에서 Client IP 를 찾아야 하는데, set_real_ip_from 에 있는 IP 는 그냥 신뢰할 수 있는 IP 로 정의하고 Client IP 에서 제외된다. 이렇게 하는 이유는 X-Forwarded-For 가 다음과 같은 구조를 가지기 때문이다.
X-Forwarded-For: 213.13.24.10, 172.10.10.34
신뢰할 수 있는 IP 라는 건 Proxied IP 를 말하는 것으로 중간에 있는 서버들을 말한다. 이러한 신뢰할 수 있는 IP 가 X-Forwarded-For 에 있기 때문에 이것은 Client IP가 될 수 없다. 이 신뢰할 수 있는 IP 를 제외하고 나면 Client IP 만 남게 되고 이것을 $remote_addr 변수에 담게 된다.
이렇게하면 끝났으면 좋을련만, Nginx 뒤에 있는 Spring Boot3 에 req.getRemoteAdd() 함수에는 여전히 Client IP가 잡히지 않는다. Nginx 에 $remote_addr 은 어디까지나 Nginx 내에서만 활용되는 것이고 뒷단에 연결된 서버까지 연향을 주지 않는다.
이 경우에는 별도의 Header 를 정의해서 그 Header 값을 가지고 옴으로써 Client IP를 얻을 수 있다. 먼저 Nginx 에서 Header 값을 지정해줘야 한다. 다음과 같이 proxy_set_header 를 이용해 X-Real-IP 에 $remote_addr 값을 담아 준다.
Nginx 에서 설정을 하게 되면 결국 Spring Boot3 에서 Nginx 에서 넘겨주는 Client IP 를 담고 있는 별도의 Header 값을 가지고 와야 하는 코드를 작성해야 한다.
Spring Boot3 에 Tomcat 설정
원래 Tomcat 에서는 Valve 를 이용해서 Real IP 를 가지고 올 수 있다. 놀랍게도 이 방법은 Nginx 의 설정과 방법과 동일하고 문법만 다르다. 신뢰할 수 있는 IP 대역을 지정하고 체크할 Header 를 지정해 주면 된다. 이것을 Spring Boot3 에서는 application.properties 에 설정만으로 간단하게 할 수 있다.
Error: There are no Template options available to be selected.
SAM 명령어를 이용해 Golang 런타임으로 생성할려고 하면 다음과 같이 오류가 발생한다.
sam init --runtime go1.x error
ZSH
1
2
3
4
5
6
]$sam init--runtime go1.x
Which template source would you like touse?
1-AWS Quick Start Templates
2-Custom Template Location
Choice:1
Error:There are no Template options available tobe selected.
SAM 명령어에 옵션으로 –runtime go1.x 를 지정하면 위와같은 오류가 발생한다. AWS 공식문서에 따르면 이제 go1.x 는 없어졌다. 대신에 provided.al2 를 사용해야 한다.
sam init --runtime provided.al2
ZSH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
]$sam init--runtime provided.al2
Which template source would you like touse?
1-AWS Quick Start Templates
2-Custom Template Location
Choice:1
Choose an AWS Quick Start application template
1-Hello World Example
2-Infrastructure event management
3-Multi-step workflow
4-Lambda Response Streaming
5-DynamoDB Example
Template:1
Which runtime would you like touse?
1-go(provided.al2)
2-graalvm.java11(provided.al2)
3-graalvm.java17(provided.al2)
4-rust(provided.al2)
Runtime:1
Template.yml 파일 변화
–runtime 옵션에 provided.al2 를 사용하면 template.yml 파일에도 변화가 생긴다.
template.yml
YAML
1
2
3
4
5
6
7
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Metadata:
BuildMethod: go1.x
Properties:
CodeUri: hello-world/
Handler: bootstrap
Runtime: provided.al2
Metadata 가 추가 된다. Runtime 이야 옵션으로 제공한 그대로 인데, Handler 가 bootstrap 이다. 이것을 바꿔서는 안된다. 또, Runtime 을 provided.al2 로 할 경우에 Lambda 의 Go 바이너리 실행 파일의 이름은 반드시 bootstrap 이여야 한다.
실행파일 이름은 반드시 bootstrap
Golang 의 람다 실행파일은 반드시 bootstrap 이어야 한다. 따라서 다음과 같이 컴파일해야 한다.
Compiling for the provided.al2 runtime
ZSH
1
]$GOARCH=amd64 GOOS=linux go build-obootstrap main.go
bootstrap 이 아닌 경우에는 실행되지 않는다.
-tags lambda.norpc 빌드 옵션 추가
bootstrap 바이너리를 빌드할때에는 반드시 -tags lambda.norpc 를 주어야 한다. AWS 공식 문서에 따르면 go1.x 에서는 필요했지만 provided.al2 로 런타임이 변경되면서 rpc 가 필요 없어졌다고 한다. 따라서 빌드할때에는 반드시 norpc 가 되도록 옵션을 지정해 줘야 한다.
Removing the RPC dependency
ZSH
1
]$GOARCH=amd64 GOOS=linux go build-tags lambda.norpc-obootstrap main.go
변경사항이 많지는 않지만 위에 서술한 옵션들을 지키지 않을 경우에 문제가 된다. 실행파일 이름과 norpc 는 반드시 해줘야 하며 template.yml 에서도 필요한 내용이 있어야 한다. 그렇지 않으면 별것도 아닌 것으로 인해서 많은 시간을 허비하게 될 것이다.
최근들어 arm64 아키텍쳐가 인기가 많아졌다. 애플의 실리콘 반도체라고 불리는것도 arm64 기반이며 Windows 11 도 arm64 에서도 작동된다. 리눅스는 오래전부터 다양한 아키텍쳐로 포팅이되었기 때문에 arm64 를 위한 배포판도 다양하게 존재한다. 문제는 arm64 아키텍쳐를 경험하기 위해서 arm64 하드웨어가 있어야 했지만, 이제는 x86_64 기반의 가상머신을 이용하면 arm64 아키텍쳐를 게스트로 운영할 수 있다.
이 문서는 x86_64 기반 가상머신에서 arm64 아키텍쳐기반의 게스트를 실행하는 방법에 대해서 기술한 것이다.
x86_64 가상머신
x86_64 아키텍쳐 기반의 가상머신으로 리눅스 운영체제를 기반으로 KVM 을 활용하고 있다. GUI 툴로서 virt-manager, CLI 로는 virsh 를 활용해서 간단하게 게스트를 생성하고 운영하고 있다. arm64 아키텍쳐 게스트를 운영하기 위한 호스트로서 x86_64 아키텍쳐 기반 가상머신 스펙은 다음과 같다.
OS: Ubuntu 22.04
Kernel: 5.15.0-207.156.6.el9uek.x86_64
CPU: AMD Ryzen 7 2700X Eight-Core Processor
libvirt vs QEMU
arm64 아키텍쳐기반 게스트를 운영하기 위해서는 QEMU 를 사용해야 한다. QEMU 는 가상머신 에뮬레이터라고 생각하면 된다. 문제는 QEMU 는 CLI 기반만 제공한다.
반면에 libvirt 는 KVM/QEMU 등을 지원하는 일종의 핸들러 라이브러리고 생각하면 된다. libvirt 를 이용하면 xml 기반으로 게스트 관련 가상머신 스펙을 정의할 수 있으며 virt-manager 와같은 GUI 툴도 활용할 수 있다.
QEMU 에뮬레이터라고 했기 때문에 arm64 를 위한 QEMU 에뮬레이터를 설치하면 arm64 기반 게스트 운영체제를 운영할 수 있다. 다음과 같이 arm64 를 위한 QEMU 에뮬레이터를 설치해준다.
qemu-system-arm 설치
ZSH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$sudo apt install qemu-system-arm
Reading packagelists...Done
Building dependency tree...Done
Reading state information...Done
The following packages were automatically installed andare no longer required:
처음부터 Arm64 기반 OS 를 게스트로 설치하면서 생성할 수도 있다. 하지만 이미 있는 Arm64 기반 OS를 가져다 쓸 수도 있다. 여기서는 Amazon Linux 를 가져다 사용해 보도록 하겠다.
Amazon Linux 는 Amazon 이 AWS 서비스에서 사용할 목적으로 만든 배포판이다. 현재 Amazon Linux2 와 Amazon Linux3 등 다양한 버전을 제공한다. 최근 프로젝트가 Amazon Linux2 를 많이 사용하고 있어서 Amazon Linux2 Arm64 기반 OS 이미지를 게스트로 한번 실행 보도록 하겠다.
Amazon Linux2 는 기본적으로 ec2-user 라는 시스템 계정이 있으며, 로그인을 위해 패스워드 방식이 아닌 SSH 인증키 방식을 사용한다. 하지만 배포되는 이미지에 맞는 인증키가 따로 없기 때문에 부팅과정에서 이 부분을 변경하도록 해야하는데, 이를 위해서 seed.iso 를 만들어 게스트 OS 에 CD-ROM 에 넣어 부팅해준다. 이 부분에 대한 설명은 다음의 페이지에서 찾을 수 있다.
systemctl 은 리눅스에 서비스 데몬을 설정하는 것과 유사하다. 과거 init script 를 systemd 로 전환하면서 만들어진 것인데, 문제는 시스템의 서비스 데몬 등록이라서 대부분 root 권한으로 실행된다. 만일 일반 사용자가 systemd 유닛으로 등록하기 위해서는 어떻게 해야 할까…
~/.config/systemd/user
사용자 홈디렉토리에 ~/.config/systemd/user 디렉토리를 생성한다. 이 디렉토리에 사용자 systemd 유닛 파일을 작성해야 한다.
test.service
systemd 유닛 파일의 확장자는 service 다. 그리고 반드시 다음과 같이 WantedBy 값을 default.target 으로 해줘야 한다. 가끔 multi-user.target 으로 하는 경우가 있는데, 이걸로 할 경우 부팅시에 자동으로 실행되지 않는다.
test.service
INI
1
2
[Install]
WantedBy=default.target
서비스 활성화
이제 다음과 같이 사용자 서비스를 활성화 해준다. 이렇게 함으로써 부팅시에 자동으로 서비스가 시작된다.
서비스 활성화
ZSH
1
]$systemctl--user enable test.service
이렇게 세팅을 하게되면 리눅스 서버가 부팅될때마다 사용자 서비스도 함께 자동으로 실행이 된다.
루트(root) 사용자 systemctl –user 사용하기
systemctl –user 는 일반사용자가 사용하는 명령어다. 하지만, root 사용자가 systemctl –user 명령어를 사용할 수 있을까? systemd 248 (released March 2021) 버전에서 -M username@ 옵션을 통해서 root 사용자가 명령어를 실행할 수 있다.
systemctl --user
ZSH
1
]# systemctl --user -M user1@ status myunit.service