Outsider's Dev Story

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

기존에 사용 중인 인프라를 Terraform으로 가져오기

Terraform으로 AWS 등의 인프라를 관리할 때 지금까지는 도구를 소개하려고 설정 파일을 만들고 plan, apply를 거쳐서 적용하는 식으로 설명했는데 실제로 사용하려면 보통 이미 사용하고 있는 인프라가 있다. 처음부터 Terraform으로 다 만든 것이 아니라면 AWS에 이미 EC2에 실행한 인스턴스나 VPC, IAM 설정 등이 이미 있을 것이다. 그리고 Terraform으로 사용하더라도 권한관리를 엄격하게 하는 게 아니라면 웹 콘솔이나 다른 도구로 만들게 되는 자원이 있게 마련이다.

Terraform을 제대로 활용하려면 기존에 있는 리소스도 Terraform으로 가져와서 관리하에 두어야 한다.

Terraform의 설정과 상태

Terraform 관련 글을 최근에 계속 쓰고 있지만 상기 차원에서 설명하면 Terraform이 AWS 등의 자원을 관리할 때 크게 설정(configuration)과 상태(state)가 있다. 여기서 설정은 .tf 파일을 얘기하고 상태는 .tfstate파일을 의미한다.

예를 들어 resource "aws_instance" "server" { }처럼 HCL로 리소스를 xxx.tf파일에 정의한다. 이를 terraform plan으로 비교하고 terraform apply로 적용을 하면 실제 AWS에 EC2 인스턴스가 시작되고 적용한 내용을 terraform.tfstate 파일에 기록한다. 이렇게 되면 AWS의 자원을 Terraform으로 관리할 수 있게 된다. 이후부터는 이 설정 파일과 상태 파일을 비교하고 실제 AWS에 적용된 내용을 비교해서 어떤 부분을 추가하고 삭제할지를 결정하게 된다.

역으로 이미 있는 자원을 Terraform으로 가져오려면 이 설정과 상태를 다 가져와야 한다는 의미이다.

Terraform import

AWS에 실행되어 있는 EC2 인스턴스

AWS EC2에 3개의 인스턴스가 있다고 해보자. 이를 Terraform이 관리하도록 가져올 예정이다. 이때 사용하는 명령어가 terraform import이다.

The current implementation of Terraform import can only import resources into the state. It does not generate configuration. A future version of Terraform will also generate configuration.

문서에 나와 있듯이 현재는 상태(state)에만 가져올 수 있고 설정(configuration)은 만들어 주지 않는다. 나중에는 지원할 거라고 하지만 아직은 관련 소식을 듣지 못해서 근 시일 내에 될지는 모르겠다. 예전 글에서 import를 어떻게 쓰는지 모르겠다고 했는데 기존 리소스를 가져오면서 이제야 이해를 했다.

Terraform의 aws_instance 문서를 보면 $ terraform import aws_instance.web i-12345678처럼 사용방법을 볼 수 있다. 각 리소스 별로 import에 대한 설명이 없는 일도 있는데 지금까지 사용해 보면 문서에 없어도 대부분 동작한다.

$ terraform import aws_instance.web i-12345678

위 명령어를 보면 aws_instance.web는 EC2 인스턴스에 지정하는 이름이다. resource "aws_instance" "web" { }처럼 .tf 파일을 만들어서 적용할 때처럼 원하는 이름을 지정하면 된다. i-12345678는 AWS에서 만든 인스턴스 ID다. 다시 풀어서 설명하면 i-12345678 인스턴스를 aws_instance.web라는 이름으로 가져오겠다는 뜻이다. 그리고 위에서 인스턴스 ID를 지정했듯이 한 번에 딱 한 개만 가져올 수 있으므로 지금처럼 인스턴스가 3개 있으면 3번 가져와야 한다.

$ terraform import aws_instance.server1 i-017eb8d5ec586a067

aws_instance.server1: Importing from ID "i-017eb8d5ec586a067"...
aws_instance.server1: Import complete!
  Imported aws_instance (ID: i-017eb8d5ec586a067)
aws_instance.server1: Refreshing state... (ID: i-017eb8d5ec586a067)

Import success! The resources imported are shown above. These are
now in your Terraform state. Import does not currently generate
configuration, so you must do this next. If you do not create configuration
for the above resources, then the next `terraform plan` will mark
them for destruction.

EC2 인스턴스를 성공적으로 가져왔다. 현재 폴더를 보면 다음과 같은 terraform.tfstate 파일이 생긴 걸 볼 수 있다. 기존에 terraform.tfstate 파일이 없었지만, 자동으로 만들어 준다.

{
    "version": 3,
    "terraform_version": "0.9.6",
    "serial": 0,
    "lineage": "20c7dcdf-4258-4834-ab4c-54bc1b6fa67e",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "aws_instance.server1": {
                  ...
                }
            },
            "depends_on": []
        }
    ]
}

이어서 나머지 EC2 인스턴스도 가져와 보자.

$ terraform import aws_instance.server2 i-08c157a39fc14205a
aws_instance.server2: Importing from ID "i-08c157a39fc14205a"...
aws_instance.server2: Import complete!
  Imported aws_instance (ID: i-08c157a39fc14205a)
aws_instance.server2: Refreshing state... (ID: i-08c157a39fc14205a)

Import success! The resources imported are shown above. These are
now in your Terraform state. Import does not currently generate
configuration, so you must do this next. If you do not create configuration
for the above resources, then the next `terraform plan` will mark
them for destruction.
$ terraform import aws_instance.server3 i-0ad45000df4f10d63
aws_instance.server3: Importing from ID "i-0ad45000df4f10d63"...
aws_instance.server3: Import complete!
  Imported aws_instance (ID: i-0ad45000df4f10d63)
aws_instance.server3: Refreshing state... (ID: i-0ad45000df4f10d63)

Import success! The resources imported are shown above. These are
now in your Terraform state. Import does not currently generate
configuration, so you must do this next. If you do not create configuration
for the above resources, then the next `terraform plan` will mark
them for destruction.

이미 terraform.tfstate가 있으면 알아서 잘 합쳐준다. 여기서 성공했다고 메시지가 나오지만, 마지막에 보면 다음과 같은 문구가 있다.

If you do not create configuration for the above resources, then the next terraform plan will mark them for destruction.

설정 파일을 만들지 않으면 terraform plan을 할 때 지워질 거라는 경고이다.(경고인데 녹색으로 잘 보이지도 않게 알려준다. ㅠ)

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

......

- aws_instance.server1

- aws_instance.server2

- aws_instance.server3


Plan: 0 to add, 0 to change, 3 to destroy.

상태에는 추가되었지만, 설정이 없으므로 Terraform은 이 인스턴스를 지워야 한다고 판단한 것이다. 실제로 여기서 terraform apply를 하면 인스턴스가 제거되므로 조심해야 한다.

이제 해야 할 일은 위 3개의 인스턴스에 대한 Terraform 설정을 만들어야 한다.

Terraforming

원래 이 설정은 처음 작성할 때와 마찬가지로 손으로 직접 작성해야 한다. 하지만 이를 도와주는 terraforming이라는 Ruby 프로젝트가 있다.(안타깝게도 terraforming은 AWS만 지원한다.) gem install terraforming로 설치하면 terraforming이라는 명령어를 쓸 수 있다. EC2 인스턴스를 가져오는 명령어는 terraforming ec2이다.

$ terraforming ec2
resource "aws_instance" "i-08c157a39fc14205a" {
    ami                         = "ami-923d12f5"
    availability_zone           = "ap-northeast-1a"
    ebs_optimized               = false
    instance_type               = "t2.micro"
    monitoring                  = false
    key_name                    = "keypair"
    subnet_id                   = "subnet-000000"
    vpc_security_group_ids      = ["sg-000000", "sg-111111"]
    associate_public_ip_address = true
    private_ip                  = "10.10.1.13"
    source_dest_check           = true

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 20
        delete_on_termination = true
    }

    tags {
    }
}

resource "aws_instance" "i-017eb8d5ec586a067" {
    ami                         = "ami-374db956"
    availability_zone           = "ap-northeast-1a"
    ebs_optimized               = true
    instance_type               = "c4.4xlarge"
    monitoring                  = false
    key_name                    = "keypair"
    subnet_id                   = "subnet-000000"
    vpc_security_group_ids      = ["sg-000000", "sg-111111"]
    associate_public_ip_address = true
    private_ip                  = "10.10.1.110"
    source_dest_check           = true

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 50
        delete_on_termination = true
    }

    tags {
    }
}

resource "aws_instance" "bastion" {
    ami                         = "ami-afb09dc8"
    availability_zone           = "ap-northeast-1a"
    ebs_optimized               = false
    instance_type               = "t2.nano"
    monitoring                  = false
    key_name                    = "keypair"
    subnet_id                   = "subnet-000000"
    vpc_security_group_ids      = ["sg-000000", "sg-111111"]
    associate_public_ip_address = true
    private_ip                  = "10.10.1.112"
    source_dest_check           = true

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 8
        delete_on_termination = true
    }

    tags {
        "Name" = "bastion"
    }
}

생략하면 헷갈릴까 봐 그대로 다 적었다. terraform import와는 달리 terraforming은 한 서비스의 자원을 한꺼번에 가져오므로 여기서 필요한 자원을 가져다 써야 한다. 위에서 보듯이 AWS에 설정된 내용을 그대로 가져온 것이므로 이름이나 참조 관계 등은 알아서 지정해 주지 않는다. 처음부터 작성하는 대신 teraforming 한 설정을 바탕으로 작성해서 노력을 조금 줄일 수 있는 정도이다. --tfstate 옵션을 사용하면 설정을 가져오면서 terraform.tfstate에 추가해주기도 하지만 나는 왠지 terraform import 쪽이 더 신뢰가 가서 이쪽을 사용한다.

당연히 여기서 terraform plan을 해도 제대로 되지 않는다. 여기서부터는 이제 설정을 실제와 고쳐가면서 terraform plan을 했을 때 No changes. Infrastructure is up-to-date.가 나올 때까지 계속 수정하면 된다.

이렇게 기존 인프라를 가져올 때 느낀 점을 얘기하면...

  • terraforming이 가져올 때 가끔 못 가져오는 부분이 있다. 이런 부분은 직접 Terraform 문서를 보고 추가해야 한다.
  • Terraform에서 지정하지 않으면 기본값을 가질 수가 있다. 이런 부분이 기존 인프라가 다르다면 이를 수정하려고 한다. 이 부분도 문서를 보고 추가해야 한다.

설정을 추가하는 부분이 좀 피곤하지만 계속하다 보면 보통 No changes 상태를 만들 수도 있고 간단한 업데이트 정도는 그냥 적용해서 맞추기도 한다.

2017/05/30 03:17 2017/05/30 03:17

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