Packer 를 이용한 EC2 AMI 생성하기

Packer 는 Terraform 으로 유명한 HashiCorp 에서 만든 프로비저닝(Provisioning) 툴 이다. Packer 는 다양한 IT 리소스를 지원하기 위해서 플러그인(Plugin) 제공하며 사용해야 한다. AWS 에 EC2 AMI 생성을 위해서 Packer 의 AWS 플러그인을 사용해야 한다.

AWS EC2 는 운영체제를 기반으로 생성되는 자원임으로 운영체제에 대한 설정도 있어야 한다. OS 의 많은 설정들을 Packer 는 Ansible 을 이용해 해준다. 따라서 Packer 를 이용한 EC2 AMI 를 생성하기 위해서는 Ansible 도 할 수 있어야 한다. 당연히 Packer 에서 Ansible 을 사용하기 위해서는 플러그인을 이용해야 한다.

Python3.12 설치

Ansible 이 필요한데, 되도록이면 Python3.12 를 설치하는게 좋다. Ansible 을 가장 최신판으로 사용하길 원하기 때문인데, Ansible 을 이용해서 OS 설정을 자동화 해야하는데 최신의 리눅스에 각종 설정컴포넌트들은 최신판에 대부분 구현 된다.

그렇다고 Ansible 을 무조건 최신판을 사용하는게 능사는 아니다. 최신의 Ansible 의 경우에는 ‘yum’ builtin 모듈이 제거 됐다. 레드햇 기반의 배포판의 경우에는 이제 dnf 를 다 사용함으로 인해서 yum 을 제거한 것인데, 과거의 레드햇 기반 배포판을 여전히 사용하고 있다면 최신판 사용은 권장되지 않는다.

Ansible 은 Python 에 기반함으로 Python 을 설치해서 pip 명령어를 이용해 설치해주면 된다. 물론 Virtual Environment 를 만들어서 해줘도 된다.

디렉토리 구조

검색을 해보면 Packer 를 사용하기 위해 디렉토리 구조를 정의하기도 한다. 하지만 그렇게 전문적으로 하는게 아닌지라, 디렉토리 구조를 별도로 정의하지 않는다.

대신, Packer 를 위한 파일과 Ansible 을 위한 파일 그리고 Python 에 가상디렉토리 환경을 위해서 각각 디렉토리를 정의해줄 필요는 있다.

Ansible 의 디렉토리 구조는 Packer 설정에서 Ansible 의 루트 디렉토리를 지정해줌으로써 Packer 가 인식하게 된다. 디렉토리 구조는 대략 다음과 같다.

전형적인 Ansible 의 표준적인 구조다. library 에는 커스텀 모듈을 작성해서 넣으면 된다. 이러한 디렉토리 구조와 더불어서 Ansible 을 위한 설정 파일은 대략 다음과 같다.

복잡하고 많은 설정은 필요하지 않다. 기본적인 설정만 있으면 동작하는데 무리가 없다. 접속자 ID 나 포트는 어짜피 Packer 에서 정의된것을 사용하기 때문에 별 다른 의미가 없다. 디렉토리 구조는 Packer 사용하는데도 적용되니 적절하게 지정해 줘야 한다.

AWS Role

만일 맥을 사용하거나 리눅스를 데스크탑으로 사용하고 있다면 AWS EC2 AMI 생성을 위한 IAM 계정에 Access Key, Secret Key 를 가지고 있을 것이다. IAM 계정에는 다음과 같이 AWS EC2 AMI 생성을 위한 정책(Policy) 가 적용되어 있어야 한다.

대략 위와같은 권한을 가지고 있으면 된다.

Packer 파일 작성

한가지 주의해야할 것이 있다. Packer 파일은 Json 형식과 HCL 형식 두가지를 지원한다. HCL 은 HashiCorp 에서 만든 문법인데, 당연히 이것을 권장하고 있다. Json 형식도 사용하지 못할 정도는 아니여서 충분히 사용이 가능하다. 그럼에도 HCL 형식을 사용하는 이유는 HCL 이 단순하게 메타 프로그래밍 수준이 아니라 각종 함수(Function) 을 제공하고 이것을 HCL 파일에서 사용할 수 있다는데 있다. 예를들어 다음과 같은 것이 있다.

formatdate 함수를 이용해서 날짜를 생성하고 이것을 AMI 파일 이름으로 사용하도록 설정했는데, Json 형식에서는 formatdate 함수를 사용할 수 없다.

또, HCL 형식으로 Packer 를 이용할 경우에 프로비져닝별로 디렉토리를 생성하는 것이 좋다. Packer 는 HCL 에 대해서 파일 이름을 미리 정의해 놓고 있다. varialbes.pkr.hcl 의 경우에는 변수 선언을 위한 파일이고, build.pkr.hcl 의 경우에는 빌드를 위한 파일 이름으로 미리 정의해 놓고 있다. 이러한 파일 이름을 미리 정의하게 되면 디렉토리내에서 자동으로 인식을 하게 되어서 편리하다.

HCL 파일로 작성

여기서는 그런 디렉토리를 생성하지 않고 빌드를 위한 파일과 변수 파일 두가지로 나뉘는 구조로 작성을 할 예정이다. 이렇게 한 후에 Packer 명령어에서 옵션과 인수값으로 파일 두개를 던져주면 실행이 된다.

일단 빌드 파일은 EC2 AMI 를 생성하기 위한 정보들을 가지고 있고 변수 파일은 빌드 파일내에서 변수들의 값을 가지는 파일이라고 보면 된다. 빌드 파일의 기본적인 구조는 대략 다음과 같다.

코멘트로 달았지만 세가지 부분으로 나뉠 수 있다. HCL 은 세부분을 각각의 파일로 나뉘어 작성하는 것도 가능하다. 하지만 복잡하고 크게하지 않을 거라면 이렇게 파일 하나에 다 담아도 된다.

중요하게 봐야 하는 부분이 변수를 선언하는 부분이다. 변수 선언과 함께 Default 값도 정의할 수 있다. 변수 할당을 위한 별도의 파일을 작성하는데 변수 선언부분은 반드시 있어야 한다. 이 부분이 없으면 Undefined Variables 에러가 발생할 것이다.

두번째로 눈여겨 봐야하는 부분은 extra_arguments 부분인데, 이는 Ansible 을 위한 옵션이라고 보면 된다. Ansible 은 Packer 가 명령행으로 실행을 시켜준다. 이때에 Ansible 에 명령행 옵션을 주고 싶다면 위와 같이 주면 된다. Ansible 은 명령행 옵션을 받아서 다양한 동작을 하게 되는데, 위 예제에서 –extra-vars 옵션을 사용해서 값을 주고 있다. 이 값들은 Ansible 내에서 변수로 사용할 수 있다. 심지여 Jinja 템플릿에서도 사용이 가능하다. 위 예제에서 os, namespace 변수를 주고 있는데, 값을 Packer 변수 파일에 값으로 넣어서 넘겨주는 구조를 취했다.

이렇게 작성한 파일을 ec2_ami.pkr.hcl 파일로 저장했다.

변수 설정을 위한 파일은 변수에 값을 다 넣어야 하는 파일을 말한다. Packer 메뉴얼에는 *.pkrvars.hcl 확장자를 가진 파일로 설명하고 있다. 이 파일은 형식이란게 별도로 없고 그냥 왼쪽에는 변수 오른쪽에는 값을 적어서 넣으면 되는 구조다.

여기에 적혀있는 변수는 ec2_ami.pkr.hcl 파일에서 정의한 변수명이여야 한다. 선언되었지만 값이 없는 경우에는 에러가 발생한다.

이렇게 작성한 파일을 web_ami.pkrvars.hcl 로 저장했다.

이제 파일은 다 작성이 되었다. 이렇게 작성된 파일은 다음과 같은 명령어로 실행하면 EC2 AMI 를 생성해 준다.

위와 같이 변수 파일과 실행파일을 명령행 옵션과 값으로 주면 실행을 해준다.

JSON 파일로 작성

JSON 파일 형식은 HashiCorp 에서 더 이상 지원하지 않는 방향으로 가고 있다. 현시점(2024.11) 에서 그나마 지원이 되어서 잘 작동되었다. (너무 기본적인 것만 사용해서 그런지 아무런 문제가 없었다.)

JSON 파일 형식도 여러파일로 쪼갤 수 있을지 모르겠지만, 그냥 하나의 파일에 모든것을 담아도 된다. 인터넷을 검색해보면 대부분 파일 하나에 모두 집어 넣고 사용하는 예제가 많다.

하나의 파일을 전부 다 넣었다. 변수를 선언하고 이것을 다른데서 사용하기 위해서 user 를 사용하고 있다는 것을 눈여겨 봐야 한다. 이렇게 하나의 파일에 전부 작성을 했으면 다음과 같이 실행할 수 있다.

packer 는 EC2 인스턴스를 생성하고, Ansible 을 이용해 EC2 에 각종 설정을 하고, EC2 를 중지하고, AMI 를 생성, EC2 삭제하는 절차를 수행하면서 AMI 를 생성해 준다.

Systemd 유닛, 부팅 후 5분 후에 실행시키기

간혹 이런게 필요할때가 있다. 부팅을 하고 난 후에, 그것도 한 10분 후 혹은 5분 후에 프로그램을 한번만 실행시키고 싶을때…. 보통 이런 것들은 시스템 모니터링을 위해 시스템의 정보를 원격 서버에 등록하는 것과 같은 것이 있다.

과거에는 /etc/rc.local 에 스크립트를 등록해서 해결했다. 하지만 rc.local 도 SystemV Init 의 유산이라, Systemd 로 변경되면서 사라졌다.

그러면 부팅 후 실행 혹은 부팅하고 5분, 수분, 그러니까 몇분이 지난 후에 한번 실행시키는 방법은 없을까?

Systemd Timer

Systemd 는 Timer 라는 게 있다. Systemd 유닛에 여러 타입이 있는데, 일반적으로 service 를 타입을 많이 쓴다. service 타입은 그야말로 데몬으로 등록하도록하는 것이다. Spring Boot 를 시작/중지 시키기등을 할때에 service 유닛을 만든다.

Timer 는 이름에서와 같이 시간에 의존해서 작동된다. 놀랍게도 timer 유닛은 오랫동안 그리고 현재도 리눅스에 사용되고 있는 cron 을 대체할 수도 있다.

특정 시점에서 실행시키기도 하고 몇분마다 반복적으로 실행되게 할 수도 있다. 아예 날짜를 지정해 특정 날짜가 되면 실행이되도록 할 수도 있다. 서론에서 말했던 부팅하고 난 후에 몇분 후에 실행되도록 할 수도 있다.

timer 유닛은 이름 그대로 시간만 잰다. timer 유닛은 유닛 이름과 동일한 service 유닛을 설정된 시간에 실행시켜주는 역할만 한다. 그래서 timer 유닛은 동일한 service 유닛과 함께 사용된다.

그러면 ‘부팅을 한 후 5분 후에 service 유닛을 실행시키기’ 를 한번 해보자.

‘한번만 실행된다’ 주목해서 service 유닛에서 실행 타입을 ‘oneshot’ 으로 해줘야 한다. 다음 예제와 같다.

이름은 간단하게 ‘a.service’ 로 했다. service 유닛의 위치는 /etc/systemd/system 디렉토리다. 이제 service 유닛을 부팅 후에 5분이 된 시점에 실행되도록 timer 유닛을 다음과 같이 작성한다. 파일 이름은 a.timer 다.

단순하다. 중요한건 유닛의 이름이 같아야 한다.

주의사항

한가지 주의사항이 있다. service 유닛을 timer 유닛이 실행시켜주는 구조로 되어 있다보니 service 유닛은 ‘enabled’ 상태여서는 안된다. enabled 상태란, 부팅과 동시에 자동 실행 설정이다. 그런데, timer 유닛은 특정 시점에서 service 유닛을 실행시켜야 하기 때문에 service 유닛이 부팅과 동시에 자동으로 실행되면 안된다.

따라서, service 유닛은 ‘disabled’ 상태여야 하고 timer 유닛은 ‘enabled’ 상태여야 한다.

Timer 유닛 리스트, 상태

timer 유닛에 어떤게 있는지를 다음과 같이 확인할 수 있다.

timer 유닛에 스케줄 상태를 다음과 같이 확인할 수있다.

몇가지 예제

다음과 같은 예제가 있다.

위 설정은 부팅 후 5분후에 처음 실행되고, 이부터는 1시간 간격으로 실행된다.

참고

create a systemd startup script that delays 30 minutescreate a systemd startup script that delays 30 minutes

유용한 Git 명령어

유용한 Git 명령어 정리.

Git 설정

대부분 Git 설정은 명령어로 한다.

하지만 Git bash 를 이용할 경우에 설정 파일을 직접 편집하도록 할 수도 있다.

브랜치 리스트

로컬 브랜치 삭제

원격 브랜치 삭제

원격 브랜치 삭제는 branch 명령어를 사용하지 않는다.

로컬에 원격 브랜치 목록 업데이트

원격 브랜치 목록 보기를 했을때에 실제 원격 브랜치와 차이가 있을 수 있다. 이것때문에 헷깔릴 수 있는데, 원격 브랜치 목록을 로컬에 반영하기 위해서는 다음과 같이 해준다.

prune 을 자동으로 되도록 설정할 수 있다.

git 설정 파일에서는 다음과 같이 설정할 수 있다.

Spring boot 에 systemd 유닛 만들기

요즘 프로젝트를 하고 있는데, 역시나 자바 시스템이 있다. 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

프로젝트에서 쓰는 방법

현재 프로젝트에서는 jar 실행을 위해서 스크립트 파일을 만들었다. 대략 다음과 같다.

자바 옵션인 -D 옵션과 jar 실행 파일을 인자로 주고 있다. 그런데, 특이하게도 데몬화를 하지 않았고 많은 초보자식 설정인 nohup 도 없다. 이렇게되면 프로그램이 포그라운드(Foreground) 실행이되어서 쉘이 묶이게 된다.

어떻게 했지? 봤더니 systemd 에 이 실행파일을 넣고 systemd 를 통해서 시작/중지를 하고 있었다. 다음과 같다.

‘ExecStart=’ 에 쉘 스크립트 실행 파일을 지정해 주고 있다.

왜 이렇게 해야 하나? 왜? 이해 할 수 없는 구조다.

Spring 공식 문서

Systemd 유닛에 등록하는 방법은 Spring 공식문서에도 나와 있다.

57.1.2 Installation as a systemd service

이 문서에 나온 systemd 유닛 파일은 프로젝트 유닛 파일과 다르다. 공식 문서에 내용은 그냥 아주 기초적인 내용만 적혀 있다. 실제 프로젝트에 적용하기에는 무리가 있다.

설정파일 작성

Spring boot 를 위한 설정을 systemd 유닛에서 사용할 수 있는 별도의 파일로 작성한다. 이 설정은 JAVA_OPTS 의 내용을 담으면 된다.

이것을 이제 systemd 에서 사용하면 다음과 같다.

공식문서에 나와 있는 내용과 EnvironmentFile 속성으로 외부에서 JAVA_OPTS 를 지정한 내용을 읽어오도록 하면 충분히 사용가능해 진다.

프로젝트에서 처럼 스크립트로 한번 감싸고 이것을 다시 systemd 유닛에서 실행되도록 할 필요가 없다는 것이다.

몇가지 개선 설정

공식문서는 간단한 예이다. 실행이 가능한 정도의 유닛 파일일 뿐이다. systemd 를 이용해 자바 애플리케이션을 실행할 때에는 여러가지를 고려해야 한다.

일단 자바 애플리케이션은 네트워크를 기반으로 한다. 따라서 네트워크 온라인 상태여야 한다. 또, StdOut, StdErr 를 Journal 로 내보내도록 해야 한다.

CloudWatch Log 총 용량 계산하기

CloudWatch Log 의 총 용량의 사용량을 알아야 얼마 정도 비용이 나가는지를 추정할 수 있다. CloudWatch Log 화면에서 총 용량을 보여주면 고맙겠지만 그런게 없다보니 비용이 얼마정도 나가는지도 모르게된다.

Python 을 이용해서 총 용량을 계산해보자.

nextToken

AWS SDK 를 사용하는데 있어 항상 나오는 것이 nextToken 이다. AWS 는 무제한으로 API Call 을 하도록 허용하지도 않고 결과의 모두를 출력해주지 않는다. 다음을 보자.

describe_log_groups 메소드에 인자값으로 limit 가 있다. 이 값은 Default 값이 있다. 문제는 이 인자값을 50 이상을 지정할 수가 없다는 것이다. CloudWatch Log 개수가 50개 이상이라면 딱 50개 만 출력이되는데, 나머지를 출력하고 싶다면 nextToken 인자를 주면 된다.

일종의 Pagenation 개념으로 다음 페이지를 넘기며 출력을 하도록 하면 전체 CloudWatch Log 를 가지고 올 수 있다.

storedBytes

CloudWatch Log 에 SDK 에 리턴값은 Dictionary 로 나온다. 여기에는 logGroups 키로 하는 리스트형태의 Dictionary 인데, 여기에는 logGroupName, storedBytes 이며 storedBytes 값을 다 더하면 된다.

이런식으로 totalStoredBytes 에 전체용량을 저장하고 출력하면 된다.

전체소스

전체 소스는 다음과 같다.

좀 더 개선된 코드를 생각해 볼 수 있다. 그건 여러분이들 해보라.

node_exporter 설치하기

node_exporter 는 OS 에 대한 각종 지표를 수집해주는 exporter 다. Prometheus 가 읽어 갈수 있도록 작은 웹서버로 작동된다.

설치야 바이너리로 배포를 하기 때문에 아키텍쳐에 맞게 다운받아서 설치를 하면 된다. 압축 풀고 시작하면 그만일 정도로 아주 간단하다. Prometheus 에 exporter들은 대부분 간단하다. 복잡하게 설치하지는 않는다.

그런데… 고려해야하는 부분이 존재한다. 일단, 설치부터…

Download & Install

다운로드는 Github 저장소에 받으면 된다.

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 파일에 다음과 같이 내용을 적어준다.

/etc/sysconfig/node_exporter 는 RedHat 기반에 적합하다. Ubuntu 면 /etc/sysconfig 디렉토리가 없기 때문에 Ubuntu 레이아웃에 맞는 곳에 넣으면 된다.

systemd 유닛 파일은 별거 없다.

딱 보면 별거 없다….. 하지만,,, 240 이하에서는 출력되는 로그들… 일명 Standard Out 들을 어떻게 처리할까? 그냥 이대로 둬도 되나? 결론은 되긴 한다. 이렇게 그대로 두면 node_exporter 가 뭔가를 출력하면 stdout, stderr 로 내보낸다. 그러면 systemd 는 이것을 /var/log/syslog 파일에 기록하게 된다.

하지만, systemd 로 변경되면서 나온 journal 에 기록을 하고 싶을지도 모른다. 이왕이면 그렇게하는게 좋기도 하다. 그래서 다음과 같이 [Service] 세션에 StandardOutput 옵션을 준다.

콘솔에도 출력을…. 뭐.. 이건 옵션이다. 여기서 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 을 사용하길 권한다.

[번역] Nginx $proxy_add_x_forwarded_for and real_ip_header

이글은 다음의 글을 번역한 것입니다.

나는 대략 다음과 같은 구조로 Nginx 와 또 다른 프론트 로드 밸런서 뒤에 webapp 을 가지고 있다.

Client(a.a.a.a) -> LB(b.b.b.b) -> NGX(c.c.c.c) -> WEBAPP(d.d.d.d)

이것이 내 Nginx 설정의 일부다.

1. 로드 밸런서는 X-Forwarded-For 필드에 Client IP 를 추가 한다.

    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 은 다음과 같은 헤더를 수신한다.

    내가 필요한 것은 먼저 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 를 세팅할 수 있습니다.

    답2

    나도 같은 문제로 여기까지 왔는데, 이러한 현상은 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.

    당신은 b.b.b.b 를 신뢰하도록 만들었다. 그래서 기대했던 a.a.a.a, b.b.b.b 가 a.a.a.a, a.a.a.a 로 바뀐 것입니다. 다음의 소스가 나에게 확신을 주었다. https://serverfault.com/questions/314574/nginx-real-ip-header-and-x-forwarded-for-seems-wrong

    X-Forwared-For 에서 Client IP 얻기

    Spring Boot3 에서 Reactive Web 이 아닌 그냥 Web 을 한다고 하면 Servlet 기반이고, Spring Boot 앱을 실행했을 경우 기본적으로 Embedded Tomcat 이 구동된다.

    만일 다음과 같은 환경이라고 가정해 보자.

    Client —-> AWS ALB ——> Nginx —–> Spring Boot3 (Embedded Tomca)

    ‘어느 미친놈이 저런 구조로 아키를 설계하고 구현하냐?’ 라고 비아냥될 수도 있지만 실제로 이렇게 하는 곳이 있다. 이런 구조에서 Spring Boot3 개발자은 다음과 같은 요구사항을 인프라에 전달 한다.

    Real Ip 를 가지고 올 수 있게 해주세요.

    Spring Boot3 에서 Real Ip, 그러니까 Client 의 IP 를 가지고 오는 방법으로 다음과 같은 코드를 자주 사용한다.

    이렇게 해서 돌려보면 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를 넣을 수 있다.

    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 값을 담아 준다.

    이렇게 하면 다음과 같은 코드로 Client IP 를 가지고 올 수 있다.

    Nginx 에서 설정을 하게 되면 결국 Spring Boot3 에서 Nginx 에서 넘겨주는 Client IP 를 담고 있는 별도의 Header 값을 가지고 와야 하는 코드를 작성해야 한다.

    Spring Boot3 에 Tomcat 설정

    원래 Tomcat 에서는 Valve 를 이용해서 Real IP 를 가지고 올 수 있다. 놀랍게도 이 방법은 Nginx 의 설정과 방법과 동일하고 문법만 다르다. 신뢰할 수 있는 IP 대역을 지정하고 체크할 Header 를 지정해 주면 된다. 이것을 Spring Boot3 에서는 application.properties 에 설정만으로 간단하게 할 수 있다.

    다음과 같다.

    internal-proxies 는 신뢰할 IP 설정이라고 보면 되고, Nginx 에서 set_real_ip_from 설정이며, remote-ip-header 는 어느 헤더에서 찾을지를 지정하는 것으로 Nginx 에서 real_ip_header 와 같다.

    이렇게 하면 Tomcat 은 Servlet 객체에 Client Ip 를 담아주게 되고 req.getRemoteAddr() 호출만으로, 추가적인 코딩 없이 Client Ip 를 가지고 올 수 있게 된다.

    AWS Sam with Golang

    AWS Lambda 를 작성하는 방법, 정확하게는 Serverless 의 API 를 작성하는 방법으로 SAM 을 사용한다. 그런데, SAM 을 이용해서 Runtime 을 Golang 으로 하려고 할 경우에 잘 안되는 경우가 발생한다. 2023년 말에 변화가 있었다.

    이 문서는 위 문서의 내용을 요약한 것이다.

    Error: There are no Template options available to be selected.

    SAM 명령어를 이용해 Golang 런타임으로 생성할려고 하면 다음과 같이 오류가 발생한다.

    SAM 명령어에 옵션으로 –runtime go1.x 를 지정하면 위와같은 오류가 발생한다. AWS 공식문서에 따르면 이제 go1.x 는 없어졌다. 대신에 provided.al2 를 사용해야 한다.

    Template.yml 파일 변화

    –runtime 옵션에 provided.al2 를 사용하면 template.yml 파일에도 변화가 생긴다.

    Metadata 가 추가 된다. Runtime 이야 옵션으로 제공한 그대로 인데, Handler 가 bootstrap 이다. 이것을 바꿔서는 안된다. 또, Runtime 을 provided.al2 로 할 경우에 Lambda 의 Go 바이너리 실행 파일의 이름은 반드시 bootstrap 이여야 한다.

    실행파일 이름은 반드시 bootstrap

    Golang 의 람다 실행파일은 반드시 bootstrap 이어야 한다. 따라서 다음과 같이 컴파일해야 한다.

    bootstrap 이 아닌 경우에는 실행되지 않는다.

    -tags lambda.norpc 빌드 옵션 추가

    bootstrap 바이너리를 빌드할때에는 반드시 -tags lambda.norpc 를 주어야 한다. AWS 공식 문서에 따르면 go1.x 에서는 필요했지만 provided.al2 로 런타임이 변경되면서 rpc 가 필요 없어졌다고 한다. 따라서 빌드할때에는 반드시 norpc 가 되도록 옵션을 지정해 줘야 한다.

    변경사항이 많지는 않지만 위에 서술한 옵션들을 지키지 않을 경우에 문제가 된다. 실행파일 이름과 norpc 는 반드시 해줘야 하며 template.yml 에서도 필요한 내용이 있어야 한다. 그렇지 않으면 별것도 아닌 것으로 인해서 많은 시간을 허비하게 될 것이다.

    Running a arm64 of guest on x86_64 host via kvm

    최근들어 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 에뮬레이터를 설치해준다.

    위 패키지를 설치가 완료되면 다음과 같이 virt-manager 에서 게스트를 생성할때에 Architecture options 이 생긴다.

    qemu-system-arm 패키지 설치후 virt-manager 에서 Architecture options 이 나타난다.

    arm64 를 위한 지원 파일 생성(Optional)

    Ubuntu22.04 에서는 이 과정이 필요하지 않다.

    arm64 게스트 실행을 위해서 파일이 필요하다. 첫번째로 NVRAM 변수들을 저장하기 위한 플래쉬 볼륨(Flash volume) 을 생성해야 한다.

    두번째로 ARM UEFI 펌웨어 파일이 필요하다.

    이제 필요한 사항은 모두 갖춰졌다.

    Amazon Linux Arm64 이미지 실행

    처음부터 Arm64 기반 OS 를 게스트로 설치하면서 생성할 수도 있다. 하지만 이미 있는 Arm64 기반 OS를 가져다 쓸 수도 있다. 여기서는 Amazon Linux 를 가져다 사용해 보도록 하겠다.

    Amazon Linux 는 Amazon 이 AWS 서비스에서 사용할 목적으로 만든 배포판이다. 현재 Amazon Linux2 와 Amazon Linux3 등 다양한 버전을 제공한다. 최근 프로젝트가 Amazon Linux2 를 많이 사용하고 있어서 Amazon Linux2 Arm64 기반 OS 이미지를 게스트로 한번 실행 보도록 하겠다.

    Amazon Linux2 는 다음과같이 다운로드 할 수 있다. 이미지 파일은  Amazon Linux 2 virtual machine images 에서 찾을 수 있다.

    seed.iso 만들기

    Amazon Linux2 는 기본적으로 ec2-user 라는 시스템 계정이 있으며, 로그인을 위해 패스워드 방식이 아닌 SSH 인증키 방식을 사용한다. 하지만 배포되는 이미지에 맞는 인증키가 따로 없기 때문에 부팅과정에서 이 부분을 변경하도록 해야하는데, 이를 위해서 seed.iso 를 만들어 게스트 OS 에 CD-ROM 에 넣어 부팅해준다. 이 부분에 대한 설명은 다음의 페이지에서 찾을 수 있다.

    간단하게, 시스템 호스트 이름(나중에 변경하면 된다) 과 ec2-user 에 로그인 패스워드를 변경하도록 seed.iso 파일을 만들어 보도록 하겠다.

    먼저 작업을 위한 디렉토리 seedconfig 만들고 meta-data, user-data 파일을 생성한다.

    로그인이 가능하도록 필요한 부분만 넣었다. 이제 다음과 같이 seed.iso 파일을 생성해 준다.

    virt-manager 로 Amazon Linux2 생성하기

    이제 준비할 것들은 모두 갖췄다. virt-manager 를 이용해서 Amazon Linux2 를 구동해 보자.

    새로운 게스트 생성하기

    OS 이미지가 있기 때문에 Import 로 하고 Arm64 는 aarch64 로 선택, Machine Type 은 virt 다.

    amazon linux2 이미지, 운영체제 종류 선택

    다운로드 받은 amazon linux2 이미지와 운영체제 종류를 선택한다. 운영체제 종류를 잘 모르겠다면 Generic Linux 로 해도 된다.

    Memory, Cpu 는 사양에 맞게 조절해주고 다음을 선택한다.

    Customize configuration before install 필수

    가상머신 이름을 지정하고 네트워크 선택을 한다음에 반드시 Customize configuration before install 를 체크해 준다. 그리고 Finish 를 하면 다음과 같은 화면이 나온다.

    Firmware 를 Custom 선택

    Firmware 를 Custom 으로 하고 no-secboot.fd 선택해 준다. 그리고 설치를 시작해 준다.

    정상적으로 부팅

    정상적으로 부팅이 된다. 로그인이 안되기 때문에 앞에서 만든 seed.iso 파일을 이용해야 한다. 이를 위해서 Amazon Linux2 게스트 OS 에 CD-ROM 하드웨어를 추가해 준다.

    CD-ROM 하드웨어 추가

    이제 VM 을 다시 시작시키면 seed.iso 에 user-data 에 설정한 ec2-user 의 패스워드로 로그인이 가능해 진다.

    정상적으로 로그인 성공

    정상적으로 로그인이 성공했다. ec2-user 는 기본적으로 sudoer 권한을 가지고 있기 때문에 sudo 를 이용해서 root 작업을 할 수 있다.

    sshd 에 설정에서 패스워드 인증을 활성화 해준다.

    설정을 변경해 줬기 때문에 sshd 를 재시작 해준다.

    이렇게 sshd 로 패스워드 인증방식을 활성화 했기 때문에 원격에서 접속도 가능해진다.

    마지막으로 Amazon Linux2 게스트 를 셧다운 해주고 seed.iso 를 위한 CD-ROM 하드웨어를 삭제해준다.