[번역] 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 하드웨어를 삭제해준다.

    systemctl –user 설정하기

    systemctl 은 리눅스에 서비스 데몬을 설정하는 것과 유사하다. 과거 init script 를 systemd 로 전환하면서 만들어진 것인데, 문제는 시스템의 서비스 데몬 등록이라서 대부분 root 권한으로 실행된다. 만일 일반 사용자가 systemd 유닛으로 등록하기 위해서는 어떻게 해야 할까…

    ~/.config/systemd/user

    사용자 홈디렉토리에 ~/.config/systemd/user 디렉토리를 생성한다. 이 디렉토리에 사용자 systemd 유닛 파일을 작성해야 한다.

    test.service

    systemd 유닛 파일의 확장자는 service 다. 그리고 반드시 다음과 같이 WantedBy 값을 default.target 으로 해줘야 한다. 가끔 multi-user.target 으로 하는 경우가 있는데, 이걸로 할 경우 부팅시에 자동으로 실행되지 않는다.

    서비스 활성화

    이제 다음과 같이 사용자 서비스를 활성화 해준다. 이렇게 함으로써 부팅시에 자동으로 서비스가 시작된다.

    이렇게 세팅을 하게되면 리눅스 서버가 부팅될때마다 사용자 서비스도 함께 자동으로 실행이 된다.

    루트(root) 사용자 systemctl –user 사용하기

    systemctl –user 는 일반사용자가 사용하는 명령어다. 하지만, root 사용자가 systemctl –user 명령어를 사용할 수 있을까? systemd 248 (released March 2021) 버전에서 -M username@ 옵션을 통해서 root 사용자가 명령어를 실행할 수 있다.

    참고

    Start a systemd user service at boot

    Run systemctl –user commands as root

    REST 역사

    현대의 소프트웨어 아키텍쳐로 인기가 있는, 아니 어쩌면 전부일 수도 있는 REST 에 대한 역사는 2000년으로 거슬러 올라간다. 시기적으로 그 당시에 한국에서는 IT 버블이라는 현상도 있었던 만큼 IT 관련 기업들이 많이 생겨나고 했던 때다.

    2000년 당시를 회상해보면 웹 메일(Web Mail) 서비스를 기반으로 하는 포털들이 많이 있었다. 해외에서는 야후(Yahoo) 가 대표적이였고 마이크로소프트(Microsoft) 의 경우에도 Hotmail 과 함께 MSN 포털을 운영했었다. 국내에서는 다음(Daum) 이 한메일(Hanmail) 서비스를, 네이버(Naver)가 네이버 메일을 기반으로 포털로서 성장하던 때다. 아,,, 네이트도 있었는데, 당시 네이트는 네이트온(NateOn) 메신저가 인기가 있었던 것으로 기억한다.

    그 당시에 해외에서도 그렇지만, 소프트웨어 아키텍쳐, 좀 더 정확하게는 이기종간에 데이터 전송을 위한 아키텍쳐로 SOAP(Simple Object Access Protocol) 를 기반으로 구축되었다. 물론 XMLRpc 도 존재했지만 엔터프라이즈 서비스에서는 SOAP 이 거의 표준처럼 사용되던 때였다.

    캘리포니아 대학에 Roy Tomas Fielding 이라는 사람이 “Architectural Styles and the Design of Network-based Software Architectures” 논문을 쓴다. 여기에서 서버간의 데이터 전송에 대한 현재까지의 기술을 고찰하고 REST 에 대해서 처음으로 기술하게 된다. 이때가 2000년이였다.

    REST 는 HTTP 를 기반으로 하기 때문에 서버의 기종과는 아무런 상관없이 데이터 전송이 가능했다. 더 군다나 그 구조자체가 SOAP 비해서는 매우 단순했기 때문에 데이터 전송에 대한 오버헤더도 줄었다.

    이후에 REST 를 기반으로 구축한 업체들이 나타나게 되는데, eBay 가 그중 하나였다. eBay 는 그들의 파트너들에게 필요한 정부를 REST API 형태로 제공했던 것이다. 그때가 2000년 11월즘이였다고 한다. 이후에 Amazon, Flicker 등이 REST 기반 서비스를 제공하게 된다.

    sam build 시 SSL 접속 에러 해결 방

    다음과 같이 sam build 시 SSL 접속 에러가 발생할 수 있다.

    이럴때에는 실행하는 위치에 pip.conf 파일을 다음과 같은 내용으로 만든다.

    그리고 이 파일에 대한 환경변수를 지정해 준다.

    다시 sam build 를 수행하면 정상적으로 실행이 된다.

    만일 컨테이너를 사용한다면 다음과 같이 하면 된다.

    Gitlab, Helm Chart CI 구축

    Gitlab 은 Package Registry 라고 Helm 저장소를 지원한다. Helm Chart 를 만들고 이를 자동으로 빌드해서 Helm 저장소로 Push 하도록 만들어 보자.

    Gitlab-runner

    Gitlab 은 빌드를 위해서 Runner 라는 별도의 패키지를 소프트웨어를 제공한다. 이렇게 하면 서버를 여러대 둘 수 있고 각 빌드별 특성에 맞는 설정을 가진 Runner 를 셋업할 수 있다.

    Helm 을 위해서는 당연히 helm 명령어가 설치되어 있어야 Runner 가 Helm 을 만들때 이용할 수 있다.

    gitlab-ci.yml

    Gitlab 은 CI 를 위해서 gitlab-ci.yml 파일을 이용한다. 이 파일이 있으면 자동으로 인식해 CI 를 실행해 준다. 이 파일에서 해줘야 할 것은 다음과 같다.

    • git 소스코드 clone
    • helm package 제작
    • gitlab 의 package registry 에 push

    여기서 한가지 짚고 넘어가야하는 것이 Gitlab-Runner 는 Gitlab 에 이름으로 등록을 하게 된다. 이것을 이용하기 위해서는 gitlab-ci.yml 에서 지정을 해줘야 한다. 뼈대는 대략 다음과 같다.

    기본적인 틀은 위와 같다. only 는 git 브랜치가 main 을 대상으로 한다는 것을 지정한다. tags 는 Gitlab-Runner 를 말한다. 어떤 Runner 를 사용할지는 빌드환경에 따라 다르다. 위의 경우에는 java-runner 인데, Runner 의 타입이 shell 로 되어 있다. 그래서 앞에서 helm 명령어를 설치해줬다.

    이제 stage:package 를 완성해야 한다. 다음과 같이 한다.

    기본뼈대에서 script 부분에 helm 을 패키징하기 위한 명령어를 넣으면 끝난다.

    다음으로 stage:registry 부분은 다음과 같다.

    package 를 하고 난후에 패키징된 파일의 확장자는 tgz 이다. 이것을 Shell 환경변수로 만들고 script 명령어에서 사용할 수 있다. curl 명령어중에서 –user 부분에 gitlab-ci-token 과 $CI_JOB_TOKEN 은 이미 정의된 사용자와 변수다. 이것은 Gitlab 에서 별도로 생성하지 않는다. 이미 있는 것이다. URL 에 있는 CI_API_V4_URL 과 CI_PROJECT_ID 도 이미 정의된 변수다. 알아서 맞게 파싱을 해준다.

    AWS ECS, Service 생성 오류 디버깅

    AWS ECS 에서 Cluster 를 생성하고 난 후에 Service 를 생성해야 한다. AWS ECS 는 Cluster, Service 생성시에 CloudFormation 으로 생성하게 되는데, 문제는 오류가 발생했을때다.

    “Circuit Breaker” 라고 되어 있어서 그냥 CloudFormation 이 실행하면서 예기치못한 오류가 발생한 것으로 착각할 수 있다. 하지만, 이것이 문제가 아닐 수도 있다.

    Service 생성 Event 확인

    CloudFormation 에서 “CREATE_FAILED” 라고 되어 있어도 AWS ECS 에 Service 에는 service 가 생성되어 있다. Service 를 클릭하고 들어가서 Events 탭을 클릭하면 생성이 진행되었던 과정을 볼 수 있다.

    여기서 task 로 시작하는 해쉬값 링크를 볼 수 있다. 이것을 클릭하고 들어가본다.

    이제 뭐가 문제인지를 확실하게 보인다. “pull image mainfest has bean retried 1 time(s)” 로 되어 있고 ECR 에서 이미지를 못가지고 오고 있음을 알 수 있다.