Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.

Headless Chrome의 사용방법

지난 4월 Chrome에 headless 모드가 추가되었다. 여기서 Headless 모드라는 것은 보통 웹 브라우저가 GUI로 동작하게 되는데 GUI가 없이 프로그램상으로만 돌리는 것을 의미한다. 실제 브라우저와 같게 동작하므로 JavaScript도 잘 동작하고 웹사이트를 크롤링하거나 스크린숏을 찍을 때 유용하고 브라우저 테스트를 할 때도 훨씬 빠르게 돌릴 수 있다.

그동안 headless 브라우저의 유용함을 알린 것은 PhantomJS이다. PhatomJS는 WebKit 기반의 headless 브라우저인데 사용하기 편하고 API로도 이용할 수 있어서 PhatomJS의 등장 이후 브라우저 테스트나 크롤링 등에서 많은 것들이 달라졌다. 하지만 Chrome이 직접 headless 모드를 추가함으로써 더는 PhantomJS의 의미가 없으므로 메인테이너에서 물러나겠다고 공지가 된 바 있다.

Chrome의 Headless 모드

Headless 모드는 Chrome 59부터 지원한다.(Linux, macOS는 59부터이고 Windows는 60부터 지원한다.) 이 글을 쓰는 현재 크롬 Stable의 최신 버전은 58이고 Chrome Canary는 61이다. 그래서 현재는 Headless를 사용하려면 Chrome Canary를 사용해야 한다.

사용방법은 Getting Started with Headless Chrome에 아주 잘 나와 있는데 이를 토대로 내용을 정리한다. 앞으로는 PhantomJS 대신 Headless Chrome을 사용해야 하므로 어떻게 사용하고 어디까지 지원하는지 파악하기 위해서 테스트를 해보았다.

Headless 모드의 사용

headless 모드를 사용한다는 것은 CLI에서 사용하겠다는 의미이므로 macOS에 설치된 크롬(혹은 크롬 Canary)를 사용해야 한다. Bash를 사용한다면 다음과 같이 설치된 크롬에 대한 별칭을 만들어서 CLI에서 사용할 수 있다.

$ alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
$ alias chrome-canary="/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary"

아직 크롬은 58 버전이므로 여기서는 chrome-canary를 사용한다.

DOM 출력

$ chrome-canary --headless --dump-dom https://blog.outsider.ne.kr

[0528/204228.632373:WARNING:dns_config_service_posix.cc(154)] dns_config has unhandled options!
<body id="tt-body-pages">
......

Headless 모드를 사용하려면 --headless 플래그를 지정해야 한다. 문서상으로는 현재는 --disable-gpu도 같이 지정해야 한다고 나와 있는데 Chrome Canary 61에서는 이 부분이 해결되었는지 없어도 잘 동작한다. 여기서는 DOM을 가져와야 하므로 --dump-dom를 지정했다. 출력된 DOM을 확인해 보면 서버에서 내려준 HTML만 받은 것이 아니라 JavaScript까지 다 동작한 최종 HTML을 받을 것을 확인할 수 있다.(여기서는 HTML이 너무 길어서 생략했다.) --dump-dom은 정확히는 document.body.innerHTML를 출력한다.

PDF로 저장

$ chrome-canary --headless --print-to-pdf https://blog.outsider.ne.kr
[0528/205117.667628:WARNING:dns_config_service_posix.cc(154)] dns_config has unhandled options!
[0528/205127.355566:INFO:headless_shell.cc(464)] Written to file output.pdf.

output.pdf로 저장되는데 이를 열어보면 PDF로 인쇄된 내용을 확인할 수 있다.(블로그의 프린트용 CSS도 조정 좀 해야겠네. ㅠ)

내 블로그가 인쇄된 PDF


스크린샷 저장

$ chrome-canary --headless --screenshot https://blog.outsider.ne.kr
[0528/205558.630395:WARNING:dns_config_service_posix.cc(154)] dns_config has unhandled options!
[0528/205607.521082:INFO:headless_shell.cc(464)] Written to file screenshot.png.

--screenshot 플래그를 지정하면 스크린숏을 저장할 수 있다. 자동으로 screenshot.png에 저장이 되고 다음과 같이 headless로 스크린숏을 지정할 수 있다.

headless chrome으로 찍은 스크린숏

$ chrome-canary --headless --screenshot --window-size=1280,1696 https://blog.outsider.ne.kr
[0528/205742.837245:WARNING:dns_config_service_posix.cc(154)] dns_config has unhandled options!
[0528/205752.688997:INFO:headless_shell.cc(464)] Written to file screenshot.png.

스크린숏을 찍을 브라우저 윈도의 크기를 지정하려면 --window-size=w,h로 지정하면 된다. 이를 이용하면 웹사이트를 반응형으로 만들 때 원하는 크기의 스크린숏을 종류별로 찍어서 제대로 나오는지 확인할 수 있다.

headless chrome으로 찍은 스크린숏

좀 더 자세한 플래그는 소스 코드에서 확인해 볼 수 있다. 유용해 보이는 옵션으로는 --hide-scrollbars, --proxy-server, --remote-debugging-address, --remote-debugging-socket-fd, --repl, --timeout, --user-agent가 있다.

원격 디버깅

headless Chrome으로 웹사이트를 띄워놓고 원격으로 디버깅할 수 있다. 서버 등에서 headless Chrome으로 뭔가 작업을 할 때 원격 디버깅 포트를 열 면 데스크톱에 설치된 chrome에서 접속해서 디버깅할 수 있다.

$ chrome-canary --headless --remote-debugging-port=9222 https://blog.outsider.ne.kr
[0528/211315.749706:WARNING:dns_config_service_posix.cc(154)] dns_config has unhandled options!

--remote-debugging-port=9222 플래그로 디버깅 포트를 지정하고 실행해 놓은 상태로 Chrome에서 http://localhost:9222/에 접속하면 다음과 같이 디버깅할 수 있는 목록이 나온다.

크롬에서 원격 디버깅포트에 접속한 화면

디버깅할 사이트를 클릭하면 브라우저에서 디버깅할 때와 같게 Chrome에서 개발자 도구로 디버깅을 할 수 있다.

headless chrome으로 띄운 웹사이트를 Chrome에서 디버깅


프로그래밍하기

문서를 보면 Node.js같은 코드로 headless chrome을 조작할 수 있는 방법도 안내하고 있다. 여기서는 Node.js에서 CLI 프로그램을 코드로 실행하거나 Lighthouse의 chrome launcher를 이용하는 방법을 설명하고 있는데 두 방법 모두 OS에 설치된 크롬을 그대로 이용하는 방법이다.

기존에 PhantomJS도 OS에 설치해 놓고 사용하기도 하지만 보통은 프로젝트에 내장해서 미리 OS별로 빌드된 PantomJS를 사용하는 게 더 일반적이라고 생각한다. 이렇게 하는 이유는 OS에 의존적이지 않게 하면서 애플리케이션을 배포해도 설치 및 구동을 자동화할 수 있어야 하기 때문인데 아직 이런 부분의 활용성은 headless chrome이 PhatomJS보다는 부족하다고 생각된다.

2017/05/28 21:35 2017/05/28 21:35

Terraform의 tfstate를 원격으로 관리하기

Terraform으로 인프라를 관리할 때 설정 파일을 만들고 이를 적용(apply)하면 terraform.tfstate라는 파일이 생긴다. 이는 Terraform의 설정 파일로 실제 인프라에 적용하면서 나온 결과를 기억하고 있는 상태 파일로 이후 설정 파일을 수정하거나 하면 "설정 파일", "상태 파일", "실제 인프라의 상태"를 비교해서 어떻게 적용할지를 판단하게 된다.

간단히 다음과 같이 EC2에 인스턴스를 하나 띄운다고 해보자.

// ec2.tf
resource "aws_instance" "test" {
  // ubuntu 16.04
  ami = "ami-afb09dc8"
  availability_zone = "ap-northeast-1a"
  instance_type = "t2.nano"
  key_name = "my-key-pair"
  subnet_id = "subnet-00000000"
  associate_public_ip_address = true

  tags {
    Name = "test"
  }
}

이를 적용하면 AWS에 EC2 인스턴스가 새로 생성된다.

$ terraform plan

$ terraform apply

terraform apply를 실행하면 현재 폴더에 terraform.tfstate 파일이 생긴 것을 볼 수 있다. 이 파일에는 사용한 Terraform 버전과 상태의 버전, 그리고 적용한 EC2 인스턴스의 ID, IP 등 다양한 정보가 포함되어 있다. 참고로 수정 후 terraform apply를 또 하면 나중에 복구할 수 있도록 terraform.tfstate.backup파일이 하나 생기게 된다. 이는 최소 이전 버전의 상태 파일을 알 수 있게 하는 용도이다.

terraform.tfstate 관리의 문제점

처음 이 파일을 봤을 때는 이걸 어떻게 관리하는 지가 궁금했다. 물론 처음 봤을 때는 뭐 하는 파일인지도 잘 몰랐지만 terraform.tfstate 파일이 없다면 항상 Terraform은 인프라를 새로 만들려고 할 것이므로 이 파일을 관리하고 있어야 한다.

그래서 Git에 추가해서 관리했다. Terraform 설정 파일을 모두 Git으로 관리하고 있으므로 그 적용 결과인 terraform.tfstate 파일도 Git에 넣어서 함께 관리하는 것이 자연스러워 보였다. 혼자 연습하고 할 때는 문제가 없었지만 금세 이 관리 방식에 문제가 많은 것을 알게 되었다.

  • 동시에 두 사람이 작업한다면 terraform.tfstate가 달라지게 되어 충돌이 발생할 수 있다. 이는 파일의 충돌뿐이 아니라 인프라에도 영향을 줄 수 있다.
  • Git으로 작업하면서 pull로 가져오지 않고 작업한다면 실수로 이전 terraform.tfstate위에서 작업할 수 있다.

그래서 Terraform에서는 이 파일을 원격으로 관리할 방법을 제공한다. 문서에 따르면 tfstate에는 실제 인프라를 적용한 결과가 있으므로 상황에 따라서는 tfstate에 민감한 정보가 포함될 수 있으므로 공개된 장소에서 tfstate를 관리하지 않도록 권장하고 있다.

AWS S3에 tfstate 관리하기

현재 원격 백엔드로는 Azure, Consul, etcd, AWS S3, Terraform Enterprise, Google Cloud Storage를 지원하고 있는데 여기서는 S3를 사용해 보도록 하자.

S3에 tfstate 파일을 저장하려면 tfstate를 저장할 버킷을 만들어야 한다. 이미 존재하는 버킷에 저장해도 큰 문제는 없지만 여기서는 tfstate를 저장할 버킷을 terrafom으로 생성해서 사용해 보자.

// terrafrom state 파일용 lock 테이블
resource "aws_dynamodb_table" "terraform_state_lock" {
  name = "TerraformStateLock"
  read_capacity = 5
  write_capacity = 5
  hash_key = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

먼저 DynamoDB에 테이블을 만든다. 이 테이블은 tfstate를 S3에 관리하면서 동시에 작업이 일어나지 않도록 하는 Lock 테이블이다. Lock을 사용할지 안 할지도 선택사항이기는 하지만 원격으로 상태 파일을 관리하므로 동시에 작업하면서 인프라에 문제가 생기지 않도록 Lock 테이블을 만들면 plan이나 apply를 할 때 먼저 잠그고 작업이 끝나면 잠금을 해제하게 된다.

// 로그 저장용 버킷
resource "aws_s3_bucket" "logs" {
  bucket = "kr.ne.outsider.logs"
  acl    = "log-delivery-write"
}

이는 로그 데이터를 저장할 S3 버킷을 생성하는 부분이다. 이는 이어서 terraform.tfstate용 S3 버킷에서 로깅을 켜서 누가 접근해서 작업했는지 알 수 있도록 여기에 기록을 남긴다. 로그는 다음과 같이 기록되게 된다.

4cc7edaa434aedfd5ce8d7c2403117183f8f6f88a99bd5989542b9c393592acf kr.ne.outsider.terraform.state [21/May/2017:06:12:05 +0000] 211.219.19.33 arn:aws:iam::410655858509:user/outsider F753C234341BC308 REST.GET.OBJECT test/terraform.tfstate "GET /kr.ne.outsider.terraform.state/test/terraform.tfstate HTTP/1.1" 200 - 56417 56417 39 38 "-" "aws-sdk-go/1.8.16 (go1.8; darwin; amd64) APN/1.0 HashiCorp/1.0 Terraform/0.9.5" -

아래는 실제 terraform.tfstate가 저장되는 S3 버킷을 생성하는 부분이다.

// Terraform state 저장용 S3 버킷
resource "aws_s3_bucket" "terraform-state" {
  bucket = "kr.ne.outsider.terraform.state"
  acl    = "private"
  versioning {
    enabled = true
  }
  tags {
    Name = "terraform state"
  }
  logging {
    target_bucket = "${aws_s3_bucket.logs.id}"
    target_prefix = "log/"
  }
  lifecycle {
    prevent_destroy = true
  }
}

버킷을 만들면서 버전 관리를 활성화했다. S3의 버전 관리를 키면 terraform.tfstate 파일을 변경할 때마다 S3가 알아서 예전 버전을 관리해주므로 문제가 생겼을 때 복구할 수 있다. 그리고 로깅을 활성화하고 앞에서 만든 로깅용 S3 버킷을 지정했다. 이제 이 버킷의 파일을 수정할 때마다 로깅 버킷에 데이터가 남게 된다.

이를 적용하면 필요한 S3 버킷과 DynamoDB에 테이블이 생성된다. 이제 이 버킷을 사용하도록 Terraform 설정 파일을 추가하자.

terraform {
  required_version = ">= 0.9.5"
  backend "s3" {
    bucket = "kr.ne.outsider.terraform.state"
    key = "test/terraform.tfstate"
    region = "ap-northeast-1"
    encrypt = true
    lock_table = "TerraformStateLock"
    acl = "bucket-owner-full-control"
  }
}

설정(terraform)이 terraform.tfstate을 원격으로 관리하는 설정이다. 여기서 S3에 저장한다고 지정하고 위에서 만든 S3 버킷을 지정했다. 한 버킷에서 여러 terraform.tfstate를 관리하기 위해서 키를 지정해서 계층을 주었다. encrypt를 설정하면 S3의 암호화 기능으로 암호화해서 저장하므로 혹시나 유출됐을 때 문제를 막을 수 있다. lock_table로 위에서 만든 DynamoDB의 Lock 테이블을 지정했다. 더 자세한 설정은 S3 백엔드 문서를 참고하면 된다.

Terraform 설정 중에 이 부분, 더 정확히는 terraform 키워드를 이용한 백엔드 설정이 있으면 Terraform은 terraform.tfstate를 로컬에서 관리하지 않고 원격에서 관리한다고 생각한다. 그래서 terraform plan을 실행하면 다음과 같이 오류가 난다.

$ terraform plan
Backend reinitialization required. Please run "terraform init".
Reason: Initial configuration of the requested backend "s3"

The "backend" is the interface that Terraform uses to store state,
perform operations, etc. If this message is showing up, it means that the
Terraform configuration you're using is using a custom configuration for
the Terraform backend.

Changes to backend configurations require reinitialization. This allows
Terraform to setup the new configuration, copy existing state, etc. This is
only done during "terraform init". Please run that command now then try again.

If the change reason above is incorrect, please verify your configuration
hasn't changed and try again. At this point, no changes to your existing
configuration or state have been made.

Failed to load backend: Initialization required. Please see the error message above.

이는 Terraform 설정을 초기화해야 한다는 메시지이다. 초기화는 init 명령어를 사용하는데 이는 최초 한 번만 실행하면 된다. 로컬에서 terraform.tfstate를 관리하고 있었다면 이를 백엔드로 올리면서 초기화를 하고 이미 원격에서 관리하는 Terraform 설정을 다운 받았다면 이 설정을 초기화하는 과정이 이루어진다.

$ terraform init
Initializing the backend...
Do you want to copy state from "local" to "s3"?
  Pre-existing state was found in "local" while migrating to "s3". No existing
  state was found in "s3". Do you want to copy the state from "local" to
  "s3"? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes

Releasing state lock. This may take a few moments...


Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your environment. If you forget, other
commands will detect it and remind you to do so if necessary.

초기화를 실행하면 위 메시지처럼 로컬에는 terraform.tfstate가 있고 S3에는 terraform.tfstate가 없다고 이를 복사할지를 물어보게 된다. 여기서는 최초 실행하는 단계이므로 yes를 입력하면 S3에 업로드하고 초기화가 완료된다. 초기화를 하면 현재 폴더에 .terraform/ 폴더가 생기는데 여기에는 백엔드 설정 내용이 들어있으므로 굳이 형상관리에 포함할 필요는 없어 보인다.

$ terraform plan
...

Releasing state lock. This may take a few moments...

이제 S3에 올라간 terraform.tfstate를 사용하게 되고 작업을 할 때 Lock을 관리하는 것을 볼 수 있으므로 여러 사람이 동시에 작업을 할 때의 문제를 막을 수 있다.

2017/05/22 02:01 2017/05/22 02:01