javalia.dev의 스프링 부트 서버를 구축하는 과정을 정리한 기록

서버 요구사항

  1. https 지원(.dev도메인의 주소는 웹브라우저로 열 경우 https사용이 강제된다.)
  2. http 동시 지원(사용하는 Let`s Encrypt는 인증 시에 http 통신을 요구한다.)
  3. 스프링 부트를 사용할 것(한 가지만 배워야 한다면 가장 어려운 것을 배운다. 라틴어를 배우면 유럽의 언어들이 쉬워질 것이라는 생각이다.)
  4. 8080에서 http, 8443에서 https, 8081에서 관리를 위한 actuator 서비스를 노출한다.
  5. 실행의 편의성을 위해서 스프링 부트의 내장 톰캣으로 실행한다.

도메인 구입 & 설정

javalia.dev를 godaddy.com에서 구입했다. 해당 도메인은 .io계열에 비해서 가격이 저렴했다.

AWS 에 EC2인스턴스 구축

처음에는 맨 위에 뜨는 옵션인 Amazon Linux 2를 사용했으나, certbot 설치 과정에서 의존성 패키지 버전이 맞지 않았다. 업그레이드를 시도했으나 잘 되지 않아서 기존에 되는 것을 확인한 Ubuntu로 교체했다.

AWS Elastic IP 설정

고정된 외부 아이피를 할당받아서 해당 아이피를 EC2 인스턴스에 연결한다.

AWS 보안 그룹 설정

EC2 인스턴스의 보안 그룹 설정에서 80, 443, 22 포트를 열어준다. 각각 HTTP, HTTPS, SSH이다.

AWS Route53 설정

godaddy.com과 Route53의 설정을 통해서 javalia.dev를 앞서 할당받은 고정아이피에 연결한다. 이로써 putty에서도 아이피가 아닌 javalia.dev로 접근할 수 있다. 물론 해당 방식으로 접근하기 위해서는 보안이 필요하고, ssh접근을 위한 비밀키를 아마존에서 다운로드해서 사용해야 한다.

  • DNS설정에서 GoDaddy에게는 아마존 네임서버로 가라고만 했고, 아마존 네임 서버가 비록 호스팅 영역에 대해서 고유해서 일반적인 경우 적법한 설정만으로는 다른 사람의 DNS Name Server 연결 설정을 가로챌 수는 없다. 그러나 이런 연결에 암호화 기반 인증이 없기에 악의적인 방식을 동원하면 DNS 요청에 대한 가로채기가 가능하다. 이것을 막기 위해서는 DNSSec을 적용해야 하는 것으로 보인다. 물론 실용성으로는 내 서버에 이것을 적용할 필요는 없지만 시간이 남는다면 해 보자.

iptables 설정

22번 포트의 ssh는 리눅스의 기본제공 서비스이기에 신경 쓸 필요가 없다.(배포판 설치한 리눅스의 경우엔 기본적으로 ssh가 막혀있는지는 모르겠으나 당연한 이야기지만 클라우드로 열어야 하는 AWS의 리눅스에서는 기본적으로 열려있다.) 그러나 80과 443은 그렇지 않다. 또 리눅스에서 1024 이하의 포트를 점유하기 위해서는 root권한이 필요하다. 그것을 위해서 스프링부트를 root권한으로 실행하는 것은 과한 일이다. 결국 iptables로 80과 443에 대한 접근을 톰캣의 포트로 돌려주는 것이 제일 편하다는 결론이기에, 아래와 같이 설정했다.

iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443

8081의 관리자포트는 외부에 연결되지 않는다. 이 포트로 shutdown명령을 보낼 것이기에 로컬 접근만 가능하도록 구성했다.

  • iptables가 구체적으로 어떤 서비스데몬을 사용해서 라우팅을 처리하는지도 모르겠다. ubuntu의 ufw등을 봐도 비활성화되어 있다. 이 점은 공부가 필요하다.

스프링 부트 프로젝트 설정

VisualStudio Code의 익스텐션 기능 중 하나로 스프링 부트 프로젝트를 설정했다. 메이븐이 아닌 그레이들을 사용하도록 설정했다.

정적 파일 서브 경로 추가

이 서버는 certbot이 서버의 정적 파일 경로에 챌린지 파일을 추가하도록 해서 도메인 이름을 인증할 것이다. 그렇게 하려면 스프링부트가 jar파일 외부에 대해서도 정적 파일 위치를 다룰 수 있게 해야한다. application.properties에 아래 내용을 추가한다.

spring.web.resources.static-locations=classpath:/META-INF/resources,classpath:/resources,classpath:/static,classpath:/public,file:/home/ubuntu/server-static

위 설정으로 /home/ubuntu/server-static 폴더가 정적 파일 제공 디렉토리로 추가되었다. certbot이 해당 디렉토리를 사용하도록 설정해서 톰캣이 실행중일 때 80번 포트로 접근해서 스스로 SSL인증을 갱신할 수 있도록 하면 된다.

actuator 추가

이 서버에는 로컬 8081포트를 통해서 shutdown요청을 보낼 것이기에 해당 내용에 필요한 gradle 의존성 및 properties설정을 추가한다. build.gradle 파일의 dependencies 항목에 아래 내용 추가

implementation 'org.springframework.boot:spring-boot-starter-actuator'

properties 설정. 아래와 같이 설정한다. shutdown을 활성화하고, 모든 endpoint를 url로 노출하며, 포트는 8081에서만 받고, ssl을 끈다. 이렇게 하지 않으면 기본적으로 ssl설정은 톰캣의 서비스 설정을 따라가게 된다. 그리고 그렇게 되면 해당 endpoint접근시 로컬호스트 도메인에 대한 서명이 필요하기에 곤란해진다.

management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=*
management.server.port=8081
management.server.ssl.enabled=false
server.shutdown=GRACEFUL

server.shutdown은 종료 요청을 받을 경우 즉시 종료가 아닌, 수행중이던 요청을 모두 처리하고 종료하도록 하기 위해서 필요하다.

Certbot 인증

certbot 홈페이지의 안내에 따라서 standalone상태로 인증을 진행한다. 인증을 하고나면 pem파일이 /ect/letsencrypt의 하위 경로에 생성되는데, 이 파일은 그것 자체로는 스프링부트의 내장 톰캣이 쓸 수가 없다. 물론, 외장 톰캣을 쓰고 AJP커넥터를 쓴다면 가능하다.(Spring + Tomcat7기준으로 경험 있음) 그러나 그러기에는 너무나 번거롭다. pem파일을 스프링부트의 톰캣이 쓸 수 있는 PKCS12 포맷의 .p12 확장자로 바꾸기 위해서 OPENSSL을 사용해서 다음과 같이 처리한다.

openssl pkcs12 -export -inkey privkey.pem -in cert.pem -out cert_key.p12

이 작업을 할 때 인증서 파일에 대한 새로운 비밀번호를 넣을 것을 요구받는다. 입력하고, 기억하자.

스프링 부트 SSL 설정 추가

이제 스프링에도 SSL 설정을 추가해야 한다. application.properties 파일에 다음 내용을 추가한다.

server.port = 8443
server.ssl.enabled=true
server.ssl.key-store=file:/etc/letsencrypt/live/javalia.dev/cert.p12
server.ssl.key-store-type=PKCS12
server.ssl.key-store-password=당신이_openssl_명령어에_입력한_비밀번호

이것은 8443포트로 요청을 받고, ssl을 활성화하며 사용하는 인증서 형식을 PKCS12로, 파일 경로는 외부 폴더로 지정한 것이다. 그리고 이 설정을 적용한 시점부터는 톰캣에 더이상 http연결로 접속할 수 없으며, 그래서 Certbot의 갱신도 불가능하다.

스프링 부트에 커넥터 추가

공식 문서에 따르면 설정을 어떻게 만져도 내장 톰캣을 사용할 때에는 http와 https를 동시에 받을 수 없다. 커넥터가 하나만 있기 때문이다. 그러나 커넥터를 코드로 추가하면 얼마든지 가능하다고 공식문서에서 이야기하고 있으며, 이 경우 HTTPS보다는 http커넥터를 추가하는 것이 더 쉬울 것이라고 이야기하고 있다. (출처 : https://docs.spring.io/spring-boot/docs/1.2.3.RELEASE/reference/html/howto-embedded-servlet-containers.html)

몇 가지 소스들을 뒤져 본 결과, 아래와 같은 클래스를 만들어서 커넥터를 추가할 수 있었다.

package dev.javalia.javaliaserver;

import org.apache.catalina.connector.Connector;
import org.springframework.beans.factory.annotation.Value;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

@Component
public class HTTPTomcatConfig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Value("${http.port}")
    private int httpPort;

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        // customize the factory here
        Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
        connector.setPort(httpPort);
        factory.addAdditionalTomcatConnectors(connector);
    }

}

코드가 http.port라는 속성을 요구하는 것을 알 수 있으며, 아래와 같이 application.propertie파일에 추가되었다.

http.port=8080

이로써 서버가 인증을 위한 80번 포트 http를 서브하면서 443으로 https를 정상적으로 동시에 처리하며, 로컬호스트에서는 8081로 actuator를 통해서 정상적으로 서버 정보를 받아오거나 서버를 끌 수 있다.

시작 및 종료 파일

서버가 시작할 때 원격 터미널 프로세스의 종료에 영향을 받지 않고 존재하도록 하기 위해 아래와 같이 실행한다.

nohup gradle bootRun &

서버를 종료하고자 할 때에는 앞서 설정한 actuator기능을 사용해서 아래와 같이 종료한다.

curl -X POST http://localhost:8081/actuator/shutdown

위의 내용을 적절히 파일로 만들어 저장하고 실행할 수 있다. 참고로 윈도우 PowerShell에서 사용하는 curl은 wget의 다른 이름이기에 POST요청 옵션을 사용할 수 없다. shutdown은 서버 수정 요청이기에 get이 아닌 post요청이 반드시 필요하다는 것도 기억하자.

ToDo

  • certbot이 서버가 켜져있는 동안 자동으로 갱신되도록 구성
  • 서버에 리액트 프론트엔드 추가
  • 만들고 싶은 것을 조금씩 만들어보기