Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Terraform의 provisioner 사용하기

Terraform은 Infrastructure as Code 도구라서 AWS를 사용하는 경우 VPC나 EC2 인스턴스, 시큐리티 그룹 등을 Terraform으로 관리할 수 있다. 이런 요소들은 직접 웹 콘솔에서 만들거나(API를 써도 되지만...) Terraform 도구를 사용하면 되지만 서버의 환경 설정 등의 프로비저닝 작업을 모두 Terraform으로 관리하는 것은 고민되는 접근이다.

여기서 환경 설정이란 것은 Python이나 Node.js를 설치한다거나 apt-get 업데이트를 한다거나 nginx를 설치한다거나 하는 등의 일이다. 사실 이런 작업은 Ansible이나 Chef 등이 더 적합하다고 보지만 어디까지를 Terraform으로 하고 어디까지를 Ansible이나 Chef로 할지는 각 조직에서 결정할 일이다.

이전에 Terraform을 사용할 때는 AWS에서 주로 썼기에 Packer로 프로비저닝한 머신 이미지를 AMI로 만들어서 Terraform에서 사용했기에 Terraform에서는 프로비저닝을 직접 사용하지는 않았고 잘 살펴보지도 않았다. Terraform에서도 Provisioner를 제공하고 있어서 여기서 직접 프로비저닝하거나 다른 도구와 연결해서 사용할 수 있다. 얼마 전에 Terraform으로 Digital Ocean의 Droplet 생성하면서 provisioner를 처음 사용해 보고 사용방법을 좀 더 정리해 보게 되었다. 여기서 사용한 Terraform 버전은 v0.11.2다.

Provisioner

Provisioner는 Terraform으로 리소스를 생성하거나 제거할 때 로컬이나 원격에서 스크립트를 실행할 수 있는 기능으로 0.8부터 추가되었다. 기본적으로 provisioner는 생성할 때만 실행되고 그 뒤에 업데이트되거나 하진 않는다. 그래서 provisioner가 실패하면 리소스가 잘못되었다고 판단하고 다음 terraform apply 할 때 제거하거나 다시 생성한다. provisioner에서 when = "destroy"를 지정하면 해당 프로비저너는 리소스를 제거하기 전에 실행되고 프로비저너가 실패한다면 다음 terraform apply 할 때 다시 실행하게 된다. 문서에 따르면 이 때문에 제거 프로비저너는 여러 번 실행해도 괜찮도록 작성해야 한다고 한다.

local-exec 프로비저너

local-exec 프로비저너는 이름 그대로 로컬에서 실행하는 프로비저너다. 여기서 로컬은 terraform apply를 실행하는 현재 머신을 의미한다.

resource "null_resource" "demo" {
  provisioner "local-exec" {
    command = "echo hello world"
  }
}

위처럼 .tf 파일을 작성했다. null_resource는 말 그대로 빈 리소스를 의미한다. 그래서 하나의 리소스와 묶이지 않은 프로비저닝 등을 사용하거나 할 때 사용할 수 있는데 여기서는 데모용으로 사용했다. 다른 리소스와 크게 다른 부분은 없고 provisioner "local-exec"를 지정해서 echo hello world를 실행하도록 했다.

$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + null_resource.demo
      id: <computed>


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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

null_resource.demo: Creating...
null_resource.demo: Provisioning with 'local-exec'...
null_resource.demo (local-exec): Executing: ["/bin/sh" "-c" "echo hello world"]
null_resource.demo (local-exec): hello world
null_resource.demo: Creation complete after 0s (ID: 822915894771826369)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

terraform apply를 실행하면 리소스를 생성하고 나서 프로비저너를 실행하면서 hello world가 출력된 것을 볼 수 있다.

resource "null_resource" "cluster" {
  provisioner "local-exec" {
    command = "echo hello world"
  }

  provisioner "local-exec" {
    command = "python --version"
  }
}

provisioner는 한 리소스 안에서 여러 번 정의해도 상관없고 작성한 순서대로 실행된다.

$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + null_resource.cluster
      id: <computed>


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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

null_resource.cluster: Creating...
null_resource.cluster: Provisioning with 'local-exec'...
null_resource.cluster (local-exec): Executing: ["/bin/sh" "-c" "echo hello world"]
null_resource.cluster (local-exec): hello world
null_resource.cluster: Provisioning with 'local-exec'...
null_resource.cluster (local-exec): Executing: ["/bin/sh" "-c" "python --version"]
null_resource.cluster (local-exec): Python 3.6.3
null_resource.cluster: Creation complete after 0s (ID: 6241181854377804821)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

이를 실행하면 아까처럼 hello world가 출력되고 내 로컬 머신의 Python 버전이 출력된 것을 볼 수 있다. 이 결과는 앞에 생성한 리소스를 다 지우고 다시 실행한 것이다. 생성 시에만 프로비저너가 실행되므로 이미 생성한 리소스에 프로비저너를 추가하더라도 실행되지 않는다. 더 정확히는 리소스가 변경된 것이 아니므로 이미 최신 상태라고 나오므로 실행이 되지 않는다.

아직 사용 사례는 다 모르지만 local-exec는 주로 로컬에 스크립트나 프로그램을 실행하거나 클라우드에 실행한 IP나 ID 같은 정보를 로컬 파일로 만들 때 사용하는 것으로 보인다.

remote-exec 프로비저너

remote-exec 프로비저너Terraform으로 Digital Ocean의 Droplet 생성하기에서 사용해서 실제 사용방법은 이전 글을 보아도 되긴 한다. local-exec와는 달리 remote-exec는 리소스를 생성한 후 타겟 머신에서 실행된다.

remote-execinline, script, scripts가 있는데 셋 중 한 번에 하나만 사용해야 한다. inline을 살펴보기 위해 아래처럼 AWS에 EC2 인스턴스를 실행하는 tf 파일을 작성해보자.

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_instance" "demo" {
  ami             = "ami-d39a02b5" # ubuntu 16.04
  instance_type   = "t2.micro"
  subnet_id       = "subnet-xxxxxxxx"
  security_groups = ["sg-xxxxxxxx"]
  key_name        = "your-key-pair"

  connection {
    user        = "ubuntu"
    type        = "ssh"
    private_key = "${file("~/.ssh/id_rsa")}"
    timeout     = "2m"
  }

  provisioner "remote-exec" {
    inline = [
      "hostname",
      "lsb_release -a",
    ]
  }
}

원격에 접속해서 실행하려면 어떻게 접속할지를 알아야 하므로 connection을 지정해야 한다. 지금은 sshwinrm를 지원한다는데 winrm은 안 써봐서 모르겠고 보통은 ssh를 쓸 것으로 생각한다. connectionresource 아래 선언할 수도 있고 provisioner 아래 선언할 수도 있는데 resource 아래 선언하면 해당 리소스의 모든 프로비저너가 선언한 커넥션을 사용하게 된다. 여기서는 inline으로 호스트 명과 Ubuntu의 버전 정보를 출력했다.

$ terraform apply


aws_instance.demo: Creating...
  ami:                          "" => "ami-d39a02b5"

aws_instance.demo: Still creating... (10s elapsed)
aws_instance.demo: Still creating... (20s elapsed)
aws_instance.demo: Provisioning with 'remote-exec'...
aws_instance.demo (remote-exec): Connecting to remote host via SSH...
aws_instance.demo (remote-exec):   Host: 13.113.162.115
aws_instance.demo (remote-exec):   User: ubuntu
aws_instance.demo (remote-exec):   Password: false
aws_instance.demo (remote-exec):   Private key: true
aws_instance.demo (remote-exec):   SSH Agent: true
aws_instance.demo (remote-exec): Connected!
aws_instance.demo (remote-exec): ip-10-10-1-47
aws_instance.demo (remote-exec): No LSB modules are available.
aws_instance.demo (remote-exec): Distributor ID:  Ubuntu
aws_instance.demo (remote-exec): Description: Ubuntu 16.04.3 LTS
aws_instance.demo (remote-exec): Release: 16.04
aws_instance.demo (remote-exec): Codename:  xenial
aws_instance.demo: Creation complete after 34s (ID: i-0f8e4ce616fee9e52)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

EC2 인스턴스를 생성하면 SSH 접속에 성공한 뒤에 호스트 명과 Ubuntu 버전을 출력한 것을 볼 수 있다. inline 대신 script를 사용하면 로컬에 있는 파일을 원격 서버에 복사한 뒤에 실행한다. 복잡한 스크립트의 경우 유용하고 scripts는 여러 파일을 실행해야 할 때 사용한다.

resource "aws_instance" "demo" {
  # 중략

  connection {
    user        = "ubuntu"
    type        = "ssh"
    private_key = "${file("~/.ssh/id_rsa")}"
    timeout     = "2m"
  }

  provisioner "remote-exec" {
    script = "./bootstrap.sh"
  }
}

앞에서 본 파일을 위처럼 수정한 뒤 bootstrap.sh 파일에 똑같은 명령어를 입력한 뒤에 실행하면 아까와 똑같은 결과가 나오는 것을 확인할 수 있다.

그 밖의 프로비저너

위에서 본 프로비저너가 가장 간단하게 사용할 수 있는 프로비저너고 프로비저너 문서를 보면 다양한 프로비저너가 있는 걸 볼 수 있다. 단순히 파일을 복사하거나 Chef, Salt와 연결할 수 있는 프로비저너도 존재한다.

앞에서도 얘기했듯이 어디까지를 테라폼의 영역으로 가져가고 어디서부터 프로비저닝 도구를 사용할지는 상황에 맞게 고민해 보아야 할 문제이다.

2018/01/19 18:57 2018/01/19 18:57