jekyll-import를 이용해서 기존 Blogger 기반 블로그 (blog.saturnsoft.net)의 글을 jekyll 기반의 www.saturnsoft.net 로 옮기는 건 무리 없이 처리 되었지만 아직 blog.saturnsoft.net에 접속해서 글을 읽는 경우는 어떻게 할 것인가를 생각해 보았는데, 제일 좋은 건 사용자를 www로 리다이렉션하면 좋겠다는 생각을 했다. 기존 기사에 접속하는 사용자를 새 페이지로 바로 가도록 하는 것인데, 가령

https://blog.saturnsoft.net/2009/09/nds.html

에 접속하면

https://www.saturnsoft.net/2009/09/12/nds/

로 리다이렉트하는 것이다.

기사 단위로 리다이렉트하려면 웹서버에서 설정을 하거나 HTML에서 해야 하는데, blog 호스트는 구글의 Blogger 플랫폼을 쓰는 관계로 내가 할 수 있는 일이 별로 없다. 다만 현재 saturnsoft.net은 Cloudflare 를 이용하고 있으므로 Workers를 사용하면 비교적 쉽게 구현할 수 있지 않을까 해서 도전해 보았다. 1

Workers

Cloudflare Workers가 생소한 사람도 있을텐데, 쉽게 이야기하면 자바스크립트를 작성해서 CDN 엣지 서버에서 바로 실행할 수 있도록 해 준다. 브라우저에서 사용 가능한 Service Worker 기능을 서버측에서 구현한 거라 보면 되는데 (필자도 자바스크립트는 잘 모르므로 그 이상의 설명은 어려움) 클라우드 서비스에서 요즘에 각광받는 서버리스(AWS Lambda, Google Cloud Function등) 와 유사하다고 보면 되는데 제일 큰 차이점은 자바스크립트라는 점과 몇몇 리전에서만 실행되는 서버리스와는 달리 CDN의 POP을 모두 이용하게 되므로 사용자 측면에서 응답 시간이 빠르다는 장점이 있다.

성능 관련은 Serverless Performance: Cloudflare Workers, Lambda and Lambda@Edge를 참고로 하면 된다.

현재(2019/1/19) Free Plan 에서도 추가 비용($5/월)로 이용 가능하므로 무료 서비스는 아니다. 자세한 사항은 서비스 정보를 보도록 하자.

하고 싶은 것

지금 하고 싶은 일을 정리하면 다음과 같다: 오리진 서버나 HTML 템플릿의 변경 없이 구 blog 기사 (예: https://blog.saturnsoft.net/2009/09/nds.html)에 접속하면 자동으로 www 아래의 이전된 글로 (예: https://www.saturnsoft.net/2009/09/12/nds/)으로 HTTP 301 리다이렉션하기.

blog 와 www 기사 URL간의 변환 목록 만들기

일단 필요한 것은, blog 기사에 해당하는 www의 기사 URL을 알아야 하는데 이게 미묘하게 다르다. 가령 https://blog.saturnsoft.net/2009/09/nds.html의 경우 www 에서는 https://www.saturnsoft.net/2009/09/12/nds/ 가 된다 (일자가 들어간다). 이걸 쉽게 계산할 방법은 없지만, 마이그레이션된 기사 파일을 보면 blog url 이 포함되어 있으므로 이걸 이용하면 매핑을 만들어낼 수 있다. 가령 해당 기사의 소스 파일인 _posts/2009-09-11-nds.html을 보면

date: '2009-09-11T23:53:00.000-07:00'
...
blogger_orig_url: https://blog.saturnsoft.net/2009/09/nds.html

date 는 파일명에 관계없이 글의 게시 시각이고, blogger_orig_url은 jekyll-import가 Blogger 글을 변환할 때 자동적으로 붙여 주는 원본 글의 URL이다. 원래는 URL이 http로 시작 하지만 https로 바꾸어 주었다. 나중에 나오겠지만 http를 모두 https로 리다이렉트하기 때문이다.

약간 까다로운 부분이 있는데, 예를 들어 소스에서 파일명이 2009-09-11-nds.html 인 기사의 경우 2009년 9월 11일자로 생각되고 date도 2009-09-11로 적혀 있지만 실제 jekyll이 생성한 경로명은 https://www.saturnsoft.net/2009/09/12/nds/이어서 날짜가 9월 12일로 계산된 것을 알 수 있다. 이건 jekyll 이 사이트를 만들어낼 때 블로그 글의 경로명에 들어가는 날짜는 GMT로 계산해서이다. (https://github.com/jekyll/jekyll/issues/6033) 을 보면 로컬 타임존으로 변환한다고 되어 있는데, 현재 www는 github 에서 빌드하므로 그쪽 빌드 서버의 시간대가 GMT이기 때문으로 생각된다(대부분의 서버 관리자는 시간대를 GMT로 설정한다).

어쨌든 이 부분을 주의하면서 기존의 모든 글에 대해서 매핑을 만들어 낸다. 나중에 Workers에 올릴 거라 자바스크립트로 생성하도록 하는 perl 스크립트를 만들었다. 이 스크립트는 _posts/ 아래의 모든 파일에 대해서 blogger_orig_url: 값이 존재하면 매핑을 만들고 자바스크립트 배열 형식으로 출력한다.

#!/usr/local/bin/perl

use strict;
use File::Slurp;
use Date::Parse;
use DateTime;

my @newfiles = glob "_posts/*";

print "// auto generated at ".`date`;
print "const pathmap = {\n";
foreach my $blog (sort @newfiles) {
	my $blogfile = read_file($blog);
	my $path_prefix;
	if ($blogfile =~ /date: '(.*)'/m) {
		my $ctime = str2time($1);
		my $cdate_dt = DateTime->from_epoch(epoch => $ctime);
		# convert to GMT
		$cdate_dt->set_time_zone("GMT");
		$path_prefix = sprintf "/%04d/%02d/%02d",
			$cdate_dt->year(),
			$cdate_dt->month(),
			$cdate_dt->day();
	}

	if ($blogfile =~ /blogger_orig_url: (.*)/m) {
		my $oldurl = $1;
		$oldurl =~ s/http:\/\/blog.saturnsoft.net//;
		if ($blog =~ /_posts\/(\d\d\d\d)-(\d\d)-(\d\d)-(.*)\.html/) {
			my ($yy, $mm, $dd, $name) = ($1, $2, $3, $4);
			my $newpath = "$path_prefix/$name/";
			print "  \"$oldurl\": \"$newpath\",\n";
		}
    }
}
print "  \"/\": \"/\"\n";
print "}\n";

이걸 사이트 디렉토리에서 다음과 같이 실행해서 pathmap.js를 만들어 준다.

$ perl gen-mappping-js.pl > pathmap.js

대략 이렇게 생겼다.

// auto generated at 2019년 1월 19일 토요일 01시 11분 09초 PST
const pathmap = {
  "/2008/02/4-nds.html": "/2008/02/11/4-nds/",
  "/2008/02/blog-post.html": "/2008/02/11/blog-post/",
  "/2008/02/blog-post_11.html": "/2008/02/12/blog-post_11/",
  ...
  "/": "/"
}

마지막의 "/": "/"https://blog.saturnsoft.net/https://www.saturnsoft.net/ 으로 리다이렉션하겠다는 의미이다.

Workers 설정

Workers 에 스크립트를 올리고 실행하기 위해서는 다음 두가지 정보가 필요하다.

  • 실행할 자바스크립트 텍스트
  • 해당 스크립트를 연동할 URL 위치

자바스크립트는 다음과 같이 작성해 보았다. 자세한 설명은 생략하겠지만 대략 다음과 같은 일을 한다.

  • 접속한 URL에서 경로명을 얻어서 pathmap[oldpath]의 값을 확인한다. 값이 있으면 새 URL을 조립해서 (www 아래의) 해당 URL로 리다이렉트한다. 아래 pathmap 의 첫번째 값처럼 https://blog.saturnsoft.net/2008/02/4-nds.html로 들어온 요청의 경우 https://www.saturnsoft.net/2008/02/11/4-nds/로 301 리다이렉트한다.
  • 리다이렉트하는 경우 Redirected-By: blog_redirector 헤더를 응답에 추가한다.
  • 이외의 경우 기존대로 오리진에서 해당 URL의 내용을 받아와서 출력한다.

그리고 위에서 생성한 pathmap.js의 내용을 아래 스크립트의 표시된 부분에 직접 붙여 넣었다.

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

( 부분은 위에서 만든 pathmap.js를 붙여넣은 것이다)
// auto generated at 2019년 1월 19일 토요일 01시 11분 09초 PST
const pathmap = {
  "/2008/02/4-nds.html": "/2008/02/11/4-nds/",
  "/2008/02/blog-post.html": "/2008/02/11/blog-post/",
  ...
  "/2018/07/blog-post.html": "/2018/07/10/blog-post/",
  "/": "/"
}

const newurlprefix = "https://www.saturnsoft.net"

/**
 * Redirect to new blog address
 * @param {Request} request
 */
async function handleRequest(request) {
  // Fetch the response.

  const oldurl = new URL(request.url)
  const oldpath = oldurl.pathname
  const newpath = pathmap[oldpath]

  // redirect to new url
  if (newpath) {
      const newurl = newurlprefix + newpath
      return new Response('', {
        status: 301,
        headers: {
          'Redirected-By': 'blog_redirector',
          'Location': newurl
        }
      })
  }

  // Everything is fine, return the response normally.
  const response = await fetch(request)
  return response
}

API를 이용할 수도 있겠지만 스크립트 하나 뿐이므로 Workers editor 에서 바로 붙여 넣는다. (크롬 브라우저를 써야 한다)

Worker editor

우측 패널에서 적용 전에 테스트가 가능한데, 그림을 보면 https://blog.saturnsoft.net/2008/02/4-nds.html에 대해서 올바르게 301 응답이 나오고 있음을 알 수 있다.

content-length: 0
content-type: text/plain;charset=UTF-8
location: https://www.saturnsoft.net/2008/02/11/4-nds/
redirected-by: blog_redirector

해당 스크립트를 URL에 붙이기 위해서는 Routes 메뉴에서 추가해 주면 된다. 스크립트 이름을 blog_redirector로 주었으므로, “Add Route”를 누르고 다음과 같이 설정한다.

Routes에 추가

이렇게 하면 https://blog.saturnsoft.net/* (*는 와일드카드이며 임의의 문자열에 매칭된다)로 접속하는 경우 blog_redirector 스크립트가 엣지서버에서 실행이 된다. 다시 편집기로 돌아가서 하단의 Deploy를 클릭하면 실 서비스에 적용되게 된다. Deploy 하면 모든 서버에 전달되는데 시간이 약간 소요되는데 대략 1-2분 정도면 충분할 것이다. 이제 실제로도 잘 되는지 확인한다.

% curl -v https://blog.saturnsoft.net/2008/02/4-nds.html
*   Trying 2606:4700::6812:4c87...
...
> GET /2008/02/4-nds.html HTTP/2
> Host: blog.saturnsoft.net
> User-Agent: curl/7.54.0
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
< HTTP/2 301
< date: Sat, 19 Jan 2019 11:25:28 GMT
< content-type: text/plain;charset=UTF-8
< content-length: 0
< set-cookie: __cfduid=d7c875ee5bf66000cee23d9270b6521731547897128; expires=Sun, 19-Jan-20 11:25:28 GMT; path=/; domain=.saturnsoft.net; HttpOnly
< location: https://www.saturnsoft.net/2008/02/11/4-nds/
< redirected-by: blog_redirector
< expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
< server: cloudflare
< cf-ray: 49b8f0df1fac93f0-SJC
<

잘 동작하고 있다. 브라우저에서도 잘 되는지 blog 사이트에 가서 몇가지 기사 링크를 눌러서 www의 글이 보이게 되는지 확인한다.

HTTP를 자동으로 HTTPS으로 리다이렉트

추가로 http://blog.saturnsoft.net/ 로 접속하면 HTTP로 서비스되는 걸 발견하였으므로 이것도 모두 HTTPS로만 볼 수 있도록 한다. Page Rule을 쓰면 아래와 같이 간단히 설정 가능하다.

pagerule

끝으로

기사의 URL목록을 만드는데 오히려 시간을 좀 소비했지 (특히 시간대 변환) 막상 Workers용 스크립트는 필자가 자바스크립트 경험이 많지 않음에도 생각보다 쉽게 만들 수 있었다. 이것 말고도 응답을 새로 쓰거나 다른 파일을 불러 오거나 하는 일들이 가능한데 Workers Recipes를 많이 참조 하였다. 최근에는 캐시에 저장된 객체를 제어하거나 자바스크립트 아닌 언어로 작성하기 위해 WebAssembly도 지원하고 있으므로 앞으로 유용하게 활용할 때가 있을 거라 본다.

  1. Disclaimer: 2019/1/19 현재 필자는 Cloudflare 에서 일하고 있습니다.