Judaeng

Terraform Up & Running (3-1) - 테라폼 시작하기(단일 웹 서버 배포하기, 설정 가능한 웹 서버 배포하기) 본문

DevOps/Terraform

Terraform Up & Running (3-1) - 테라폼 시작하기(단일 웹 서버 배포하기, 설정 가능한 웹 서버 배포하기)

Judaeng 2023. 7. 12. 22:00

위 제목의 책을 통해 테라폼의 기본 개념, 정의 등을 공부하고 설치에서 운영까지 알아보자.

단일 웹 서버 배포하기


인스턴스에 웹 서버를 동작시키는 것을 해보자.

우리의 목적은 최대한 간단하게 웹 서비스를 구성하는 것이며, 이 단일 웹 서버는 아래 그림처럼 HTTP 요청들에 대해 응답할 수 있도록 만들 것이다.

실제 환경에서는 웹 서버에 루비 온 레일즈(Ruby on Rails)나 장고(Django) 같은 웹 프레임워크를 사용해나 하나, 실습에서는 'Hello, World'에 응답만 할 수 있도록 최대한 간단하게 웹 서버를 구현할 것이다.

#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p 8080 &

# busybox no install

#!/bin/bash
echo "Hello, World" > index.html
nohup [python3 경로, 위치] -m http.server 8080 &

위 배시 스크립트는 index.html 파일에 'Hello, World'를 작성하고 비지박스(busybox, https://busybox.net/)를 를 수행하여 8080 포트로 웹 서버를 시작하는 명령어다.

또한, 비지박스 명령어와 nohup과 &를 추가하여 배시 스크립트가 종료되더라도 지속해서 백그라운드로 수행할 수 있도록 설정했다.

 

포트 번호

예제에서 포트 번호를 기본 포트 80 대신에 8080으로 사용한 이유는 1024 이하 모든 포트를 사용하기 위해서는 root 권한이 필요하기 때문이다.

또한, 해당 포트를 사용할 경우 서버를 손상할 수 있는 공격자가 root 권한을 가지므로 보안의 위협이 있다.

그러므로 권한이 제한된 사용자로 웹 서버를 기동시키는 것이 가장 이상적인 방법이며, 높은 숫자의 포트 번호를 사용해야 한다.

하지만 이 장의 후반부에서도 다루겠지만, 로드 밸런서에서 80 포트 번호로 트래픽을 수용하여 높은 숫자 번호로 라우팅하는 설정을 추가해야 한다.

 

EC2 인스턴스에 해당 스크립트를 어떻게 적용해야 할까?

일반적으로 앞에서 다룬 서버 템플릿 도구로 할 수 있다.

패커를 통해 직접 AMI를 구성하여 웹 서버를 설치할 때 해당 스크립트를 적용해 놓을 수 있다.

이 예제의 임시 웹 서버는 한 줄의 비지박스 명령어로 되어 있으므로 기본 ubuntu 16.04 이미지로 사용할 수 있으며, EC2 인스턴스의 사용자 데이터(user data) 설정을 통해 'Hello, World' 스크립트를 인스턴스 기동 시 수행할 수 있다.

resource "aws_instance" "example" {
  ami           = "ami-40d28157"
  instance_type = "t2.micro"
  
  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF
              
  tags = {
    Name = "terraform-example"
  }
}

# busybox no install
resource "aws_instance" "example" {
  ami                    = "ami-06ca3ca175f37dd66"
  instance_type          = "t2.micro"
  vpc_security_group_ids = ["${aws_security_group.instance.id}"]
  
  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup [python3 경로, 위치] -m http.server 8080 &
              EOF
  
  tags = {
    Name = "terraform-example"
  }
}

이 파일에서 <<-EOF 와 EOF 표시는 새로운 줄에 문자를 매번 추가하는 것이 아니라 여러 줄의 단락으로 처리하는 테라폼 히어닥(heredoc) 문법이다.

 

웹 서버를 동작시키기 전에 추가로 한 가지 작업을 해야 한다.

기본적으로 아마존 웹 서비스는 EC2 인스턴스의 들어오고 나가는 트래픽을 허용하지 않으며, 예제와 같이 8080 포트에 대한 트래픽을 허용하기 위해서는 다음과 같이 보안 그룹(Security Group)을 통해 설정해야 한다.

resource "aws_security_group" "instance" {
  name = "terraform-example-instance"
  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    }
}

이 코드는 aws_security_group 리소스를 생성하고(아마존 웹 서비스 공급자의 리소스는 aws_로 시작하는 것을 명심하자).

CIDR(사이더) 블록 0.0.0.0/0으로부터 8080 포트에 대해 TCP 요청을 받을 수 있도록 설정하였다.

CIDR 블록 IP 주소 대역을 간략하게 표현한 것이다.

예를 들어, CIDR 블록이 10.0.0.0/24인 경우 10.0.0.0부터 10.0.0.255까지의 모든 IP 주소를 표현한다.

위에서 정의한 CIDR 0.0.0.0/0은 모든 IP주소 대역이 포함된 값이며, 즉 인터넷이 연결된 어느 곳에서라도 접속할 수 있도록 설정한다는 의미다.

 

사실상 단순히 보안 그룹만 만드는 것으로는 충분하지 않으며, 실제로 EC2 인스턴스가 해당 보안 그룹을 사용할 수 있도록 해야 한다.

이를 위해 보안 그룹의 ID를 aws_instance 리소스에 vpc_security_group_ids의 변수로 지정해야 한다.

 

보안 그룹의 ID를 다음과 같이 채움 참조(interpolation) 구문으로 변수 처리한다.

"${something_to_interpolate}"

따옴표에 달러 기호와 중괄호로 묶은 표시는 일반 문자열이 아닌 특별한 방법으로 처리된다.

이 책에서는 다양한 문법 구문을 사용하며, 이번 예제에서 사용하는 것은 리소스의 속성값으로 불러오는 변수 구문이다.

 

테라폼에서는 모든 리소스를 속성값으로 불러 변수를 사용할 수 있으며(각 리소스 문서에서 가능한 변숫값들을 찾을 수 있다).

구문 문법은 아래와 같다.

"${TYPE.NAME.ATTRIBUTE}"

예를 들어, 보안 그룹의 ID를 사용하려면 다음과 같다.

"${aws_security_group.instance.id}"

aws_instacne 리소스에서 해당 보안 그룹 ID를 vpc_security_group_ids 변수로 사용할 수 있다.

resource "aws_instance" "example" {
  ami                    = "ami-40d28157"
  instance_type          = "t2.micro"
  vpc_security_group_ids = ["${aws_security_group.instance.id}"]
  
  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF
  
  tags = {
    Name = "terraform-example"
  }
}

# busybox no install

resource "aws_instance" "example" {
  ami                    = "ami-06ca3ca175f37dd66"
  instance_type          = "t2.micro"
  vpc_security_group_ids = ["${aws_security_group.instance.id}"]
  
  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup [python3 경로, 위치] -m http.server 8080 &
              EOF
  
  tags = {
    Name = "terraform-example"
  }
}

resource "aws_security_group" "instance" {
  name = "terraform-example-instance"
  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    }
}

다른 리소스에 대해 채움 참조 문법을 사용하게 된다면 암시적인 의존성을 정의해야 한다.

테라폼은 해당 의존성을 읽어 의존성 그래프(graph)를 만들고 그 기반으로 우선순위를 자동으로 정해서 리소스를 생성한다.

예를 들어, 테라폼은 EC2 인스턴스 생성을 위해 보안 그룹 ID가 필요하므로 보안 그룹이 EC2 인스턴스 리소스 전에 생성되어야 한다는 것을 알고 있다.

테라폼 graph 명령어를 통해 어떤 의존성이 있는지 확인한다.

> terraform graph

digraph {
	compound = "..."
	newrank = "..."
	subgraph "..." {...}
}

결괏값은 DOT라는 그래프 설명 언어로 되어 있으며, 다음과 같이 아래 이미지로 바꿔볼 수 있다.

데스크톱 웹이나 Graphviz 혹은 GraphvizOnline 웹으로도 확인할 수 있다.

테라폼이 의존성 트리(tree)를 따라서 수행될 때 그것 자체에서도 여러 리소스를 변수 형태로 정의할 수 있으며, 변경사항을 신속하게 적용할 수 있다.

이것이 선언형 언어의 장점이며, 원하는 것을 지정하면 테라폼이 가장 효율적인 방법으로 찾는다.

 

만약 plan 명령어를 수행하면 테라폼이 보안 그룹을 생성하며, 기존 EC2 인스턴스를 새로운 사용자 데이터를 갖는 것으로 변경한다(-/+ 의미는 '변경').

> terraform plan

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.example will be created
  + resource "aws_instance" "example" {...}

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

───────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to
take exactly these actions if you run "terraform apply" now.

테라폼에서 EC2 인스턴스 태그와 같은 메타 데이터 변경 이외에 대부분 변경 사항들은 실제로 완전히 새로운 인스턴스를 만든다.

이것은 전에 말했듯이 '서버 템플릿 도구'의 변하지 않는 인프라의 패러다임이라고 한다.

웹 서버가 바뀐다는 것은 서비스 사용자로서는 중단할 수도 있다는 의미다.

 

적용 계획이 문제없다면, apply 명령어를 통해 EC2 인스턴스가 새롭게 배포된 것을 아래의 그림에서 확인할 수 있다.

[그림]

화면 아래쪽 설명 탭을 보면 EC2 인스턴스의 IPv4 퍼블릭 IP를 확인할 수 있으며, 부팅 후 1~2분 이후 웹 브라우저나 curl 등의 도구를 통해서 해당 IP 주소에 8080 포트로 http 요청을 하여 결과를 확인할 수 있다.

- (내 생각) 실습하고 있는데, busybox가 amazon linux 이미지에 설치가 안되어있다.

그리고 python3 server를 서버가 실행될 때 서버가 시작되어야 하는데 시작이 안된다.

user data가 잘 실행이 안된다. 문제가 있지만? 아직 해결하지 못했다.🤔

EC2 서버가 실행되면서 user data 내용이 한 번만 반영된다는 것을 깨달았다.

다시 destroy하고 새로 생성하면 될 것 같다.

> curl http://<EC2_INSTANCE_PUBLIC_IP>:8080
Hello, World

Hello, World!

지금까지 아마존 웹 서비스 EC2 인스턴스에 웹 서버를 올려서 기동시켜 보았다.

네트워크 보안

이 책의 예제들은 기본 VPC와 VPC의 기본 서브넷을 사용하고 있다.

VPC는 하나 이상의 서브넷으로 이루어져 있으며, 각 서브넷 별로 별도의 IP 주소 대역을 갖는다.

기본 VPC에 있는 서브넷들은 모두 외부로 열려 있는 서브넷이며, 이는 인터넷을 통해서 IP 주소로 접근할 수 있다는 의미다.

이 이유로 실습을 수행하는 환경에서 EC2 인스턴스로 접근할 수 있다.

 

서버를 공인 서브넷에 위치시켜 빠르게 테스트할 수 있으나, 실제 환경에서는 보안 위험이 존재한다.

공격자가 인터넷을 통해 무작위로 IP 주소를 스캔하고 공격을 하기 위한 취약점을 찾는다.

만약 서버가 모두 인터넷에 연동되어 있고 실수로 특정 포트를 보호하지 않거나 보안 취약점이 있는 코드를 지속해서 사용한다면 침해당할 수 있다.

그래서 상용 시스템에서는 서버를 배포할 때, 특히 모든 데이터 저장소는 인터넷을 통해서 접근할 수 없는 사설 서브넷을 이용해야 한다.

공인 서브넷에 존재하는 서버들은 최소한의 리버스 프락시(reverse proxy) 혹은 로드 밸런서만 배치하여 공격의 가능성을 최소화해야 한다.

 

설정 가능한 웹 서버 배포하기


이전의 코드를 자세히 보면 aws_instance의 user_data 설정 부분과 aws_security_group의 ingress 설정 부분에 8080 포트에 대한 설정이 중복 작성되어 있음을 알 수 있다.

중복 배제 원칙(DRY, Don't Repeat Yourself)에 어긋나는 부분이며, 같은 변숫값은 하나로 정의해야 하며, 항상 하나로 대표되어야 한다.

이 코드의 경우 포트 수정을 위해 두 군데에 같이 변경을 해야 하지만, 한 곳만 변경하고 다른 곳에는 변경을 빠뜨릴 가능성이 높다.

 

코드를 보다 DRY 하도록 만들고 유연하게 설정할 수 있도록 작성해야 한다.

테라폼에서는 입력 변수(Input variables)로 정의할 수 있게 해 놓았다.

variable "NAME"  {
  [CONFIG ...]
}

변수 선언의 본문에는 세 개의 매개변수가 포함될 수 있으며, 매개변수는 모두 선택 사항이다.

description

이 매개변수를 통해서 변수 사용 방법을 작성하는 것이 매우 좋다.

동료들이 코드를 이해하는 것뿐만 아니라 plan, apply의 명령어를 수행할 때도 도움이 된다.

default

변수에 값을 제공하는 방법은 여러 가지가 있다.

예를 들어, 명령어와 함께 수행(-var 옵션)하거나 파일(--var-file 옵션)을 불러오는 방법, 그리고 환경변수(TF_VAR_<변수이름>)를 통해 전달하는 방법이 있다.

만약 전달되는 변수가 없다면, 변수는 이 기본값을 사용한다.

기본값마저 존재하지 않는다면 테라폼에서 사용자 입력으로 받도록 화면에 호출한다.

type

문자열, 리스트 혹은 맵(map) 값 중 하나여야 한다.

타입을 지정하지 않았다면 테라폼이 알아서 기본값의 변수 속성을 선택할 것이며, 기본값 역시 정의되어 있지 않다면 문자열로 판단한다.

 

리스트 입력 변수에 대한 예제는 다음과 같다.

variable "list_example" {
  description = "An example of a list in Terraform"
  type        = "list"
  default     = [1, 2, 3]
}

맵 타입 입력 변수의 예제는 다음과 같다.

variable "map_example" {
  description = "An example of a list in Terraform"
  type        = "map"
  
  default     = { 
    key1 = "value1"
    key2 = "value2"
    key3 = "value3"
  }
}

웹 서버 예제에서 필요한 것은 테라폼의 변수 숫자이며, 문자열로 자동으로 강제 변환되므로 형식을 생략할 수 있다.

variable "server_port" {
  description = "The port the server will use for HTTP requests"
}

server_port는 기본값이 없는 변수이며, plan, apply 명령어를 수행할 때 테라폼이 설명과 같이 입력하도록 화면에 프롬프트를 호출한다.

> terraform plan

var.list_example
  The port the server will use for HTTP requests

  Enter a value:

만약 대화형 프롬프트를 통해 입력을 받지 않는다면 명령어 줄 옵션에 -var를 통하여 변숫값을 제공할 수 있다.

> terraform plan -var server_port="8080"

매번 plan, apply 명령어를 통해 변숫값을 입력하는 것을 잊지 않기 위해서 default 설정을 정의하는 것이 효율적이다.

variable "server_port" {
  description = "The port the server will use for HTTP requests"
  default     = 8080
}

테라폼 코드에서 입력받은 변수를 처리하기 위해서는 채움 참조 구문을 다시 활용해야 하며, 다음과 같은 형태로 작성하면 된다.

"${var.server_port}"

다음은 server_port를 활용해 보안 그룹의 from_port, to_port에 변수로 입력해야 하는 예제다.

resource "aws_security_group" "instance" {
  name = "terraform-example-instance"
  
  ingress  {
    from_port    = "${var.server_port}"
    to_port      = "${var.server_port}"
    protocol     = "tcp"
    cidr_blocks  = ["0.0.0.0/0"]
  }
}

비슷한 문법으로 EC2 인스턴스에 포트 번호를 설정하는 비지박스 부분은 다음과 같이 변수 처리한다.

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup busybox httpd -f -p "${var.server_port}" &
              EOF
              
# busybox no install
  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup [python3 경로, 위치] -m http.server "${var.server_port}" &
              EOF

테라폼에서는 입력 변수 이외에 출력 변수 역시 다음과 같이 설정할 수 있다.

output "NAME"  {
  value = VALUE
}

예를 들어, EC2 콘솔에서 보는 서버의 공인 IP 주소를 출력하고 싶다면 다음과 같이 출력 변수로 설정하면 된다.

output "NAME"  {
  value = "${aws_instance.example.public_ip}"
}

또다시 채움 참조 문법을 사용해야 하며, aws_instance 리소스에서 public_ip에 대한 속성값을 참조하여 출력한다.apply 명령어를 다시 수행하고 변경된 사항이 없으므로 추가한 출력 변수가 맨 마지막에 출력된다.

> terraform apply

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.example will be created
  + resource "aws_instance" "example" {...}

  # aws_security_group.instance will be created
  + resource "aws_security_group" "instance" {...}

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

Changes to Outputs:
  + public_ip = (known after apply)

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

테라폼 apply 명령어를 통한 콘솔 출력으로 확인할 수 있다.또한, terraform output 명령어를 통해 전체 출력 목록의 값을 확인할 수 있고, 다음과 같이 출력 변수 값을 출력할 수도 있다.

> terraform output public_ip
"[EC2 공인 IP]"

이와 같은 입력, 출력 변수는 인프라형 코드를 만드는데 매우 필수적인 요소이며, 다음에 더 상세하게 활용하는 법을 배우자.

 

지금까지 작성한 코드 확인

provider "aws" {
  region = "us-east-1"
}

resource "aws_instance" "example" {
  ami                    = "ami-06ca3ca175f37dd66"
  instance_type          = "t2.micro"
  vpc_security_group_ids = ["${aws_security_group.instance.id}"]
  
  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, World" > index.html
              nohup /usr/bin/python3 -m http.server "${var.server_port}" &
              EOF
  
  tags = {
    Name = "terraform-example"
  }
}

resource "aws_security_group" "instance" {
  name = "terraform-example-instance"

  ingress {
    from_port   = "${var.server_port}"
    to_port     = "${var.server_port}"
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    }
}

variable "server_port" {
  description = "The port the server will use for HTTP requests"
  default     = 8080
}

output "public_ip" {
  value = "${aws_instance.example.public_ip}"
  description = "The public IP address of the Web"
}

 

🐝tip - Formatting 명령어

# fmt 명령어를 사용하면 terraform 구조에 맞게 코드를 정렬해준다.
> terraform fmt

 

Comments