본문 바로가기
DevOps

Infrastructure Provisioning Tool, Terraform

by NEMNE 2022. 9. 18.

과거 인프라 관리

과거에는 대부분의 인프라 관리를 온프레미스로 진행했다. 네트워크, 컴퓨팅, 냉각 설비 등의 인프라 리소스를 모두 구매하여 데이터 센터에 위치시키고 OS를 설치하거나 어플리케이션 구동에 맞게 설정하는 등의 복잡한 수동적인 절차가 필요했다.

 

인프라 관리에는 다음과 같은 문제가 발생했다.

  1. 각 인프라 분야의 엔지니어를 고용하는 비용과 서로 다른 분야의 인프라 엔지니어들이 소통하는 비용이 크게 발생했다.
  2. 갑작스럽게 인프라 리소스가 필요지면 해당 인프라를 데이터 센터까지 가져오고 환경 설정하는 작업이 오래 걸렸고 심지어 회사의 규모가 커질수록 여러 부서의 승인 절차까지 기다려야 하기 때문에 서비스 수요에 맞춰 빠르게 대응할 수 없었다.
  3. 인프라 성능에 대한 모니터링이 어려웠다. 인프라 단에 장애가 발생했을 시 장애의 원인은 어느 분야에서 발생한건지를 알 수 없었다.
  4. 여러 사람과 인프라 설정에 대해 협업을 할 때 불일치 문제가 발생한다.

 

다행스럽게도 오늘날에는 위에서 언급한 인프라 주된 문제 대부분이 클라우드 컴퓨팅의 등장으로 해결되었다.

클라우드 컴퓨팅을 통해 인프라 장비 설치 및 구성에 많은 시간과 비용을 절약할 수 있으며, 여러 리전 및 가용 영역을 두고 있는 클라우드 서비스를 통해 가용성을 확보할 수 있었고, 컴퓨팅 리소스를 발생하는 트래픽에 따라 탄력적으로 리소스를 조정함으로써 기민하게 대응할 수 있게 됐다.

 

이에 따라 기존 운영팀은 하드웨어에 많은 비용과 노력을 투자하지 않고 앤서블, 패커, 테라폼, 도커 등의 도구를 사용하여 소프트웨어 작업에 더 많은 시간을 할애하고 있다. 즉, 이에 따라 운영 팀과 개발 팀의 역할이 모호해짐에 따라 DevOps가 등장하게 됐다.

 

DevOps는 소프트웨어를 효율적으로 전달하는 프로세스로 정의할 수 있으며, 문화, 자동화, 측정, 공유에 핵심 가치를 가지고 있다.

여기서 자동화, 그 중에서도 배포 자동화는 수동으로 실행하는 방법 대신 IaC를 통해서 인프라를 관리할 수 있다.

 

 

IaC란?

→ Infrastructure as Code는 Infra 구성을 수동적인 프로세스가 아닌 코드를 통해 인프라 리소스를 구성할 수 있도록 해주는 처리 방식이다.

 

과거 인프라 관리 및 구성은 특정 관리자만 다른 개발자들은 모르는 “마법의 명령어"를 통해 구축했으며, 장애가 발생했을 때도 관리자의 도움이 반드시 필요했다. 이에 따라 협업을 진행하게 되면서 구성 프로세스에 대해 불일치가 발생하게 된다.

 

IaC는 코드를 기반으로 인프라를 작성하기 때문에 작성 및 편집이 쉬워졌으며, 버전 관리도 가능했기 때문에 불일치 문제도 사라지게 됐다.

 

IaC는 다양한 종류를 가지고 그에 따라 많은 언어로 구현할 수 있다.

  • 애드 훅 스크립트(ad hoc script)
    • 파이썬, 배시, 루비 등으로 작성된 스크립트
  • 구성관리 도구
    • 앤서블
  • 서버 템플릿 도구
    • 패커, 도커
  • 오케스트레이션 도구
    • 쿠버네티스, 도커 스웜
  • 프로비전 도구
    • 테라폼, AWS 클라우드 포메이션

위 언어를 하나만 사용하는 것이 아니라 필요에 맞게 다양한 종류들을 함께 사용할 수 있다. (e.g. 테라폼 + 앤서블, 패커 + 테라폼)

 

 

테라폼이란?

IaC로서 인프라 환경을 안정적, 효과적으로 빌드, 수정, 버저닝을 하도록 도와준다.

여기서 인프라 환경은 Instance, Stroage, Networking 등의 저수준 컴포넌트뿐만이 아니라 DNS 설정, SaaS 기능 등의 고수준 컴포넌트 역시 포함된다.

 

또한 다양한 플러그인들을 통해 다양한 클라우드 벤더의 리소스들을 프로비저닝할 수 있으며 사용자가 선언적으로 작성한 구성에 따라 프로비저닝 된다.

 

 

테라폼의 작동 방식

테라폼은 내부적으로 Core, Providers로 구성되어 있다.

테라폼을 통해 AWS 리소스를 프로비저닝하는 유스케이스를 예시로 들면 다음과 같이 동작한다.

 

getting-started.tf라는 프로비저닝할 AWS 리소스 구성과 AWS Provider를 사용할 것이라는 구성이 담긴 테라폼 파일을 생성한다.

이를 통해 AWS Provider를 사용하기 위해 API 혹은 라이브러리를 다운로드한다.

 

다시 CLI를 통해 리소스 프로비저닝 요청을 보내면 Terraform Core는 terraform.state라는 생성한 리소스의 상태가 담긴 파일을 생성하고 Terraform Providers에서 가지고 있는 AWS API, 라이브러리를 통해 AWS 상에서 실제 리소스가 생성된다.

 

여기서 기존에 생성된 리소스에서 일부 구성만 변경에서 다시 프로비저닝 요청을 보내게 되면 Terraform Core에서는 terraform.state와 비교를 통해 변경된 부분만 다시 생성하는 특징을 가지고 있다.

 

Core

  • Go로 작성된 바이너리 파일이며 terraform 명령어를 통해 접근이 가능한 CLI 툴이다. (엔트리 포인트)
  • IaC로서 설정 파일에 대해 읽거나 보관할 수 있다.
  • 리소스 상태 관리
  • 명령어 plan 실행
  • RPC를 통해 Plugins와 소통한다. → Plugin에 대해 쉽게 사용할 수 있도록 하는 고수준 프레임워크

Provider

  • 특정 벤더에 대해 접근할 수 있도록 도와주는 역할을 한다.
  • 리소스에 대한 간단한 CRUD API 호출을 담당한다.
  • 인프라 제공자를 통한 인증을 한다.

 

 

테라폼 기본 문법

 

Provider

→ 다양한 클라우드 선택 및 리전 등의 설정 가능 / 계정 설정도 여기서 가능하다.

provider "aws" {
  region = "ap-northeast-2"
  shared_config_files      = ["/Users/tf_user/.aws/conf"]
  shared_credentials_files = ["/Users/tf_user/.aws/creds"]
  profile                  = "customprofile"
}

+) 클라우드 공급자 간 투명한 이식성(transparent portability)

여러 벤더들이 존재하는 프로바이더를 보면 “서로 다른 클라우드 공급자를 똑같은 코드로 실행할 수 있는가?”라는 의문점을 가질 수 있다.

물론 어렴풋이 짐작할 수 있겠지만 클라우드마다 제공하는 서비스나 동작 방식이 다르기 때문에 똑같은 코드로 실행하기에는 어려움이 있다.

그러나 “유사한” 방식으로는 실행할 수 있다.

Resource

resource → 각 유형의 공급자마다 서버, 데이터베이스 및 로드 밸런서와 같이 다양한 종류의 리소스 존재

resource "<PROVIDER>_<RESOURCE_TYPE>" "<NAME>" {
  [CONFIG...]
}

resource "aws_instance" "test-instance" {
  ami = "ami-0ea5eb4b05645aa8a"
  instance_type = var.instance_type
  vpc_security_group_ids = [ aws_security_group.test-instance-sg.id ]

  user_data = <<-EOF
                #!/bin/bash
                echo "Hello World!" > index.html
                nohup busybox httpd -f -p ${var.service-port} &
                EOF
  tags = {
    Name = "terraform-example"
  }
}

Variables

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

variable "number_example" {
  description = "An Example of a number variable in Terraform"
  type = number
  default = 42
}

Outputs

output "<NAME>" {
  [CONFIG...]
}

output "alb_dns_name" {
  value       = aws_lb.example.dns_name
  description = "The domain name of the load balancer"
}

output "instance_name" {
  value       = aws_instance.test-instance.name
}

 

기본적인 명령어

  • terraform init : 현재 디렉토리 상에서 테라폼을 사용할 수 있도록 설정한다. (여러 번 실행해도 안정적으로 동작하기 때문에 테라폼에서는 매번 init을 하는 것을 권장하고 있다.)
  • terraform plan : 실제로 생성하게 됐을 때 문법적인 오류가 없는지, 기존 state에서 변경점은 없는지 확인해주는 역할을 한다. (그러나 실제로 생성하여 틀린 부분이 없는지 검증을 못하기 때문에 plan에서는 오류가 발생하지 않았지만 apply에서 발생하는 경우가 종종 있다.)
  • terraform apply : terraform plan에 나왔던 계획을 실제로 수행하는 역할을 한다.

 

간단한 환경을 구축하는 예제

brew install을 통해 테라폼 설치

export AWS_ACCESS_KEY_ID=

export AWS_SECRET_ACCESS_KEY=

혹은 aws CLI default 프로필을 사용

 

main.tf

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

resource "aws_launch_configuration" "alc-example" {
  image_id = "ami-0ea5eb4b05645aa8a"
  instance_type = "t2.micro"
  security_groups = [ aws_security_group.test-instance-sg.id ]

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello World!" > index.html
              nohup busybox httpd -f -p ${var.service-port} &
              EOF
  

  lifecycle {
    create_before_destroy = true
  }

}
resource "aws_autoscaling_group" "asg-example" {
  launch_configuration = aws_launch_configuration.alc-example.name
  vpc_zone_identifier = data.aws_subnet_ids.default.ids

  target_group_arns = [aws_lb_target_group.asg.arn]
  health_check_type = "ELB"

  min_size = 2
  max_size = 10

  tag {
    key = "Name"
    value = "terraform-asg-example"
    propagate_at_launch = true
  }
}

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

  ingress {
    from_port = var.service-port
    to_port = var.service-port
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

data "aws_vpc" "default" {
  default = true
}

data "aws_subnet_ids" "default" {
  vpc_id = data.aws_vpc.default.id
}

resource "aws_lb" "example" {
  name = "example-lb-tf"
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = data.aws_subnet_ids.default.ids
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.example.arn
  port = "80"
  protocol = "HTTP"

  default_action {
    type = "fixed-response"

    fixed_response {
      content_type = "text/plain"
      message_body = "404: page not found"
      status_code = 404
    }
  }
}

resource "aws_lb_target_group" "asg" {

  name = aws_lb.example.name

  port     = var.service-port
  protocol = "HTTP"
  vpc_id   = data.aws_vpc.default.id

  health_check {
    path                = "/"
    protocol            = "HTTP"
    matcher             = "200"
    interval            = 15
    timeout             = 3
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }
}

resource "aws_lb_listener_rule" "asg" {
  listener_arn = aws_lb_listener.http.arn
  priority     = 100

  condition {
    path_pattern {
      values = ["*"]
    }
  }

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.asg.arn
  }
}

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

  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

variables.tf

variable "service-port" {
  description = "the port will use ec2 http request"
  type = number
#   default = 8080
}

variable "number_example" {
  description = "An Example of a number variable in Terraform"
  type = number
  default = 42
}

variable "list_example" {
    description = "An example of a list in Terraform"
    type = list
    default = ["a", "b", "c"]
}

variable "list_numeric_example" {
    type = list(number)
    default = [ 1, 2, 3 ]
}

variable "map_exmaple" {
    type = map(string)
    default = {
        key1 = "value1"
        key2 = "value2"
        key3 = "value3"
    }
}

variable "object_example" {
  type = object({
    name = string
    age = number
    tags = list(string)
    enabled = bool
  })

  default = {
    age = 1
    enabled = false
    name = "value"
    tags = [ "value" ]
  }
}

outputs.tf

output "alb_dns_name" {
  value       = aws_lb.example.dns_name
  description = "The domain name of the load balancer"
}

 

참고

테라폼 업앤러닝 - YES24

Terraform Primer | Terraform Architecture | #2]

 

 

테라폼을 통해 AWS 비용을 개선한 사례가 궁금하다면?

 

Terraform과 Shell Script를 통해 AWS 비용 절감하기

들어가기 전에 지난 6월부터 현재까지 소프트웨어 마에스트로 과정을 진행하면서 Kubernetes 기반 MLOps 서빙 플랫폼을 제작하고 있습니다. 핵심 목표로는 모델러가 Kubernets, Backend 지식 없이 모델 버

nemne.tistory.com

 

'DevOps' 카테고리의 다른 글

HPA와 Metrics Server  (0) 2022.10.10
쿠버네티스 패키지 매니저 Helm  (0) 2022.08.17
Skaffold와 Cloud Code  (0) 2022.07.04
Kubernetes Context 적용방법  (0) 2022.06.28
chroot로 컨테이너 환경 간접 체험하기  (0) 2022.06.06