Cloud Service Practice 08

EC2 웹 서버, 로컬과 외부 접속의 비밀

Apache를 설치하고 index.html을 수정하는 명령이 많아 보이지만, 로컬 확인과 브라우저 확인이 어떤 차이를 갖는지 헷갈린다.

근거 · 교안 p55-p62

EC2 웹 서버, 로컬과 외부 접속의 비밀 대표 이미지

1장: 로컬에서는 잘 되는데?

솔라의 손가락이 엔터 키 위에서 잠시 멈췄다. 까만 터미널 화면에는 방금 전까지 그녀가 쏟아부은 노력의 흔적이 고스란히 남아있었다. EC2 인스턴스에 웹 서버를 설치하고, 실행하고, 심지어 자신만의 환영 메시지를 담은 index.html 파일까지 만들었다. 이제 가이드에 나온 마지막 확인 절차만이 남았다.

솔라는 만족스러운 미소를 지으며 망설임 없이 명령어를 입력했다.

curl localhost

화면에 응답이 즉시 나타났다. 그녀가 직접 작성한 문장이었다.

Hi, I'm Web Server 1.

“됐다! 성공!” 솔라가 자신도 모르게 작은 환호성을 질렀다. “언니, 이것 봐! 내 웹 서버가 드디어 인터넷 세상에 공개됐어! 이제 누구나 내 페이지를 볼 수 있다구!”

근처에서 책을 읽던 루나는 솔라의 들뜬 목소리에 고개를 들었다. 동생의 노트북 화면에 떠 있는 curl localhost 명령어와 그 결과를 조용히 살펴본 루나는 차분한 미소를 보였다.

“성공한 거 맞네.” 루나가 말했다. “그런데 솔라, 방금 실행한 그 명령어가 정확히 뭘 확인해 준 거라고 생각해?”

솔라는 질문의 의도를 파악하지 못하고 자신만만하게 대답했다. “당연히 웹 서버가 잘 돌아가고 있다는 뜻이지! 가이드에도 ‘웹 서버가 정상적으로 동작하는지 확인해 봅니다’라고 쓰여 있었는걸. 이제 내 컴퓨터 브라우저에서 이 서버의 Public IP 주소를 치면, 저 메시지가 뜰 거야.”

솔라는 이미 다른 브라우저 탭으로 넘어가 AWS 콘솔에 있는 인스턴스의 Public IP 주소를 복사할 준비를 하고 있었다. 그 모습은 마치 파티 준비를 막 끝내고 이제 막 현관문을 열어 손님들을 맞이하려는 호스트 같았다.

“잠깐만.”

루나가 마우스를 향하던 솔라의 손을 가볍게 막았다. “브라우저로 확인하기 전에, 우리가 지금 확인한 게 뭔지 정확히 짚어보고 가자.”

루나는 테이블 위에 놓여 있던 메모지와 펜을 가져와 커다란 사각형을 그렸다. 사각형 위에는 ‘EC2 인스턴스’라고 썼다.

“이 네모가 솔라가 만든 서버, 즉 하나의 독립된 컴퓨터라고 생각해 봐.”

루나는 사각형 안에 작은 동그라미를 그리고 ‘웹 서버 (httpd)‘라고 적었다. 그 옆에는 작은 문서 아이콘을 그리고 ‘index.html’이라고 썼다.

“솔라는 지금 터미널을 통해 이 ‘EC2 인스턴스’라는 집에 들어가 있어. 그리고 집 안에서 curl localhost라는 명령으로 ‘웹 서버’라는 로봇에게 말을 건 거지.”

루나는 ‘웹 서버’ 동그라미를 향해 안쪽에서 시작하는 작은 화살표를 그렸다. 화살표 끝에는 localhost라고 적었다.

“여기서 localhost는 ‘나 자신’이라는 특별한 주소야. 즉, 이 EC2 인스턴스라는 집 안에서, 스스로에게 ‘이봐, 웹 서버 로봇! 일하고 있나?’ 하고 물어본 거야. 그랬더니 로봇이 ‘네, 주인님. 여기 ‘index.html’ 파일 준비됐습니다.’ 하고 대답해 준 거고.”

솔라는 루나가 그린 그림을 가만히 들여다보았다. 커다란 사각형, 그 안에 있는 동그라미와 문서, 그리고 모든 것을 잇는 화살표는 전부 ‘EC2 인스턴스’라는 사각형 안에서만 맴돌고 있었다. 외부에서 들어오거나 외부로 나가는 화살표는 없었다.

“잠깐만… 그럼 이 확인 과정은 전부 이 서버 안에서만 일어난 일이란 말이야?” 솔라의 목소리 톤이 미묘하게 바뀌었다. 확신에 차 있던 목소리에서 무언가를 깨닫기 시작한 탐정의 목소리로.

“바로 그거야. curl localhost는 마치 집 안에서 문을 모두 잠그고 혼자 ‘우리 집 주방에 저녁 식사가 준비됐나?’ 하고 확인하는 것과 같아. 주방에 음식이 있다는 사실은 확인했지. 하지만 집 밖의 손님이 초인종을 눌렀을 때 우리가 문을 열어줄 수 있는지, 애초에 손님이 우리 집까지 찾아올 수 있는 길이 있는지에 대해서는 아무것도 알려주지 않아.”

솔라의 얼굴이 살짝 붉어졌다. ‘정상적으로 동작한다’는 말의 의미를 너무 넓게 해석했던 것이다. 웹 서버 소프트웨어가 켜져서 내부 요청에 응답할 준비가 되었다는 뜻이었는데, 마치 전 세계와 연결될 준비가 끝났다는 뜻으로 받아들였다.

“아… 그렇구나. curl localhost는 그냥 ‘웹 서버 프로그램 자체는 살아있다’는 생존 신고 같은 거였네.” 솔라가 중얼거렸다. “엔진에 시동이 걸렸는지 확인한 거지, 이 차가 고속도로를 달릴 수 있는지는 아직 모르는 상태인 거구나.”

스스로의 비유에 고개를 끄덕인 솔라에게 curl localhost 명령어는 더 이상 최종 성공의 증표가 아니었다. 그것은 모든 확인 절차의 끝이 아니라, 가장 첫 번째 단계의 내부 점검일 뿐이었다. 외부와의 연결은 이것과는 완전히 다른 문제라는 것을 직감했다.

“좋아. 그럼 엔진 확인은 끝났으니까… 이제 진짜로 도로에 나갈 수 있는지 시험해 볼 차례네.”

솔라는 다시 노트북으로 시선을 돌렸다. 아까 복사하려던 Public IP 주소가 화면에서 그녀를 기다리고 있었다. 이제 이 주소를 브라우저에 입력하는 행위는 ‘당연히 성공하겠지’라는 막연한 기대가 아니라, ‘과연 외부로 향하는 문은 열릴까?‘라는 명확한 질문을 던지는 다음 단계의 실험이 되었다.

2장: 브라우저 접속, 왜 안 되죠?

솔라는 이전과 다른 종류의 긴장감 속에서 마우스를 움직였다. 조금 전 curl localhost의 성공으로 얻었던 뿌듯함은, 이제 ‘외부로 향하는 문은 열릴까?’라는 명확한 질문과 함께 신중한 호기심으로 바뀌어 있었다. 루나가 그린 그림, EC2 인스턴스라는 커다란 사각형 안에서만 맴돌던 화살표가 머릿속을 떠나지 않았다.

솔라는 AWS 콘솔 창에 떠 있는 인스턴스의 Public IP 주소를 블록 지정하여 복사했다. Ctrl+C. 그리고 망설임 없이 새 브라우저 탭을 열었다. 주소창은 깨끗하게 비어 있었다. Ctrl+V. 숫자로 이루어진 낯선 주소가 붙여넣기 되었다.

이제 남은 것은 엔터 키를 누르는 것뿐. 솔라는 심호흡을 한번 하고 키를 눌렀다.

브라우저 탭의 로딩 아이콘이 빙글빙글 돌기 시작했다. 1초, 2초… curl localhost가 즉각적으로 응답했던 것과는 사뭇 다른 분위기였다. 마치 먼 곳으로 편지를 보낸 뒤 답장을 기다리는 사람처럼, 솔라는 화면의 작은 원이 멈추기만을 초조하게 바라보았다.

하지만 원은 멈추지 않았다. 몇 초가 지나고, 마침내 기다림은 익숙한 실패의 메시지로 끝이 났다.

이 사이트에 연결할 수 없음 [IP 주소]에서 응답하는 데 시간이 너무 오래 걸립니다.

“어…?”

솔라의 입에서 작은 탄식이 흘러나왔다. 그녀는 혹시나 주소를 잘못 복사했나 싶어 다시 콘솔 창과 브라우저 주소창을 번갈아 확인했지만, IP 주소는 정확했다. 새로고침 버튼을 몇 번 더 눌러보았지만, 결과는 마찬가지였다. 텅 빈 회색 화면과 차가운 에러 메시지뿐이었다.

“왜 안 되지? 분명히 서버는 살아있는데…”

솔라가 혼잣말을 하며 미간을 찌푸리자, 그 모습을 조용히 지켜보던 루나가 다시 메모지를 끌어당겼다. 거기에는 아까 그렸던 ‘EC2 인스턴스’ 사각형 그림이 그대로 남아 있었다.

“솔라, 방금 한 일을 이 그림에 한 번 더 표시해 볼까?”

루나는 펜을 들어 사각형 바깥쪽에 작은 노트북 모양을 그렸다. ‘솔라의 브라우저’라고 이름을 붙였다. 그리고 그 노트북에서부터 ‘EC2 인스턴스’ 사각형을 향해 긴 화살표를 그리기 시작했다. 화살표 끝에는 솔라가 입력한 Public IP 주소를 적었다.

“이게 바로 외부에서, 솔라의 컴퓨터에서 서버로 보내는 요청이야. 이 요청이 서버 안의 ‘웹 서버’ 로봇에게 잘 도착해서, ‘index.html’을 가지고 밖으로 나와야 성공이지.”

루나는 말을 마치며 화살표를 그렸다. 하지만 화살표는 사각형 안의 ‘웹 서버’ 동그라미에 닿지 못했다. 마치 투명한 벽에 부딪힌 것처럼, 화살표는 ‘EC2 인스턴스’ 사각형의 테두리에서 딱 멈춰 버렸다. 루나는 그 지점에 작은 ‘X’ 표시를 했다.

솔라는 그림의 변화를 뚫어지게 쳐다봤다. 그림은 현재 상황을 너무나 명확하게 보여주고 있었다.

  1. 내부 점검 (curl localhost): 인스턴스 사각형 안에서 ‘웹 서버’ 동그라미를 콕콕 찔러보는 것. (성공)
  2. 외부 연결 시도 (브라우저): 사각형 바깥에서 안으로 들어가려는데, 벽에서 막히는 것. (실패)

“아…!” 솔라가 무릎을 쳤다. “완전히 다른 문제였구나! 나는 curl localhost의 성공이 브라우저 성공으로 이어지는 길 위에 있다고 생각했어. 그냥 확인하는 위치만 다른 같은 종류의 테스트라고. 그런데 그게 아니었네. 하나는 집 안에서 ‘주방 불 켜졌나?’ 확인하는 거였고, 지금 이건 집 밖에서 초인종을 누르는데 아예 대문이 잠겨서 들어가지도 못하는 상황인 거네.”

이전까지 솔라에게 ‘웹 서버가 안 된다’는 것은 하나의 덩어리진 문제였다. 하지만 이제 그녀의 눈에는 두 개의 독립된 문제 지점이 보였다.

  • 문제 1: 웹 서버 프로세스가 죽었는가? -> curl localhost로 확인 가능. (현재 이상 없음)
  • 문제 2: 외부에서 서버로 들어오는 길이 막혔는가? -> 브라우저 접속으로 확인 가능. (현재 문제 발생)

“정확해.” 루나가 고개를 끄덕였다. “많은 사람들이 이 두 가지를 혼동해. 로컬에서 성공했다고 바로 외부에 공개될 거라고 기대하지. 하지만 방금 우리가 확인했듯이, 서버 프로그램이 살아있는 것과 외부에서 그 서버에 접근할 수 있는 것은 별개의 문제야. 그리고 실패했을 때 확인해야 할 지점도 전혀 다르지.”

이제 솔라에게 브라우저의 ‘연결할 수 없음’ 메시지는 더 이상 막연한 실패가 아니었다. 그것은 ‘웹 서버 프로세스는 정상이지만, 외부에서 인스턴스로 들어오는 네트워크 경로 어딘가에 문제가 있다’는 훨씬 더 구체적인 단서가 되었다. 문제는 더 작고 명확해졌다.

“알겠어. 그럼 내 웹 서버 엔진은 멀쩡하다는 건 확인했고… 이제 남은 질문은 하나네.”

솔라는 루나가 ‘X’ 표시를 해 둔 사각형의 테두리를 손가락으로 가리켰다.

“이 보이지 않는 벽은 대체 정체가 뭐야? 그리고 이 벽에 문을 내려면 어떻게 해야 하는 거지?“

3장: 보안 그룹: 외부로 가는 문을 열다

솔라가 그린 메모지 위에는 명확한 대조가 펼쳐져 있었다. EC2 인스턴스를 나타내는 사각형 안에서는 curl localhost 명령이 성공적으로 웹 서버에 닿았지만, 사각형 밖에서 출발한 브라우저의 접속 시도는 붉은색 ‘X’ 표시에 막혀 있었다. 로컬에서는 살아있던 서버가 외부 세계와는 단절된 상태, 그 원인이 바로 저 ‘X’ 표시된 보이지 않는 벽이었다.

루나는 말없이 솔라의 노트북 화면을 가리켰다. 인스턴스의 Public IP 주소를 확인했던 바로 그 AWS 콘솔 화면이었다. “그 벽, 이름이 있어. 그리고 솔라, 너는 이미 그 벽의 존재를 알고 있었어. 단지 그게 웹 페이지로 향하는 길도 막고 있다는 걸 몰랐을 뿐이지.”

루나는 마우스를 움직여 인스턴스 정보가 나열된 화면의 한 부분을 클릭했다. ‘보안’ 탭이었다. 화면 아래로 상세 정보가 펼쳐졌고, 그중 ‘보안 그룹’이라는 링크가 눈에 띄었다.

“이게 바로 그 벽의 이름이야. ‘보안 그룹(Security Group)’.” 루나가 말했다. “EC2 인스턴스라는 가상의 집에 설치된 디지털 경비 시스템 같은 거지. 허가된 통신만 안으로 들여보내 주는 문지기야.”

솔라는 ‘보안 그룹’ 링크를 클릭했다. 새로운 페이지에는 ‘인바운드 규칙’이라는 제목의 표가 나타났다. 표 안에는 단 한 줄의 규칙만이 존재했다.

유형프로토콜포트 범위소스
SSHTCP220.0.0.0/0

솔라는 이 규칙을 어디선가 본 기억이 났다. 바로 터미널로 이 EC2 인스턴스에 처음 접속할 때 사용했던 방식이었다.

“아! SSH… 포트 22번. 이건 내가 터미널로 이 서버에 들어올 수 있도록 열어둔 문이잖아. 그럼 지금 이 집에는 문이 딱 하나만 있다는 뜻이네? 터미널 접속용 문.”

“바로 그거야.” 루나가 확인해주었다. “그리고 우리가 쓰려는 웹 브라우저는 다른 종류의 문을 사용해. 보통 ‘HTTP’라는 이름의 80번 문을 사용하지. 그런데 저 목록에 80번 문에 대한 규칙이 있어?”

솔라는 표를 다시 쳐다봤다. 없었다. 22번 포트를 허용하는 규칙 외에는 아무것도 없었다. 그림 속에서 외부의 요청 화살표를 막아섰던 ‘X’의 정체가 드디어 밝혀지는 순간이었다. 그것은 오류나 고장이 아니었다. 기본으로 설정된, 지극히 정상적인 ‘규칙’이었다. 외부에서 오는 모든 방문객 중, ‘22번 문으로 온 손님’만 들여보내고 나머지는 모두 돌려보내는 철저한 경비원이 있었던 것이다.

“알겠다… 내 웹 서버는 집 안에서 손님 맞을 준비를 다 끝냈는데, 정작 경비원이 대문에서 웹 브라우저라는 손님을 돌려보내고 있었던 거구나.”

솔라는 ‘인바운드 규칙 편집’ 버튼을 눌렀다. 그리고 망설임 없이 ‘규칙 추가’를 클릭했다.

“여기에 80번 문을 열어주는 규칙을 추가하면 되겠네.”

솔라는 유형 드롭다운 메뉴에서 ‘HTTP’를 선택했다. 프로토콜과 포트 범위는 각각 ‘TCP’와 ‘80’으로 자동 설정되었다. 소스는 기본값인 0.0.0.0/0 (Anywhere) 그대로 두었다. 전 세계 어디에서든 이 웹 서버에 접속할 수 있도록 허용한다는 의미였다.

유형프로토콜포트 범위소스
HTTPTCP800.0.0.0/0

솔라는 ‘규칙 저장’ 버튼을 눌렀다. 이제 인바운드 규칙 목록에는 SSH와 함께 HTTP 규칙이 나란히 표시되었다. 서버로 들어올 수 있는 문이 하나 더 생긴 것이다.

두근거리는 마음으로 솔라는 아까 ‘연결할 수 없음’ 오류가 떴던 브라우저 탭으로 돌아갔다. IP 주소는 이미 입력되어 있었다. 그녀는 다른 아무것도 건드리지 않고, 오직 새로고침 버튼 하나만 눌렀다.

빙글빙글 돌던 로딩 아이콘은 1초도 되지 않아 사라졌다. 그리고 텅 비어 있던 회색 화면 위로, 솔라가 직접 작성했던 익숙한 문장이 선명하게 나타났다.

Hi, I'm Web Server 1.

“됐다! 드디어 됐어!”

솔라의 얼굴에 환한 미소가 번졌다. curl localhost 때의 성공과는 비교할 수 없는, 문제의 근원을 파악하고 직접 해결했을 때만 느낄 수 있는 짜릿한 성취감이었다.

이제 솔라에게 웹 서버를 확인하는 두 가지 방법은 명확한 역할 분담을 갖게 되었다.

  1. curl localhost: 서버 안에서, 웹 서버 프로세스가 살아있는지 확인 (엔진 시동 점검)
  2. 브라우저 접속: 서버 밖에서, 보안 그룹이 외부 요청을 허용하는지 확인 (대문 개방 점검)

솔라는 아까 루나가 그렸던 메모지를 다시 가져왔다. 그리고 펜을 들어 EC2 인스턴스 사각형 테두리에 그려져 있던 ‘X’ 표시를 지웠다. 대신 그 자리에 작은 ‘문’을 그리고, 문 위에 ‘보안 그룹’이라고 적었다. 그리고 그 문 옆에 작은 팻말처럼 덧붙였다. HTTP (80) - OPEN.

“앞으로는 서버를 만들면 항상 두 가지를 확인해야겠어.” 솔라가 스스로에게 다짐하듯 말했다. “엔진은 켜졌는지, 그리고 외부로 향하는 문은 열어뒀는지.”

더 이상 ‘로컬에서 되면 외부에서도 되겠지’라는 막연한 기대는 없었다. 내부의 동작과 외부로의 공개는 별개의 검증이 필요한, 명확히 구분된 두 단계라는 사실을 이제 그녀는 온전히 이해했다.