728x90
반응형

테라폼 backend용 s3 버킷 생성

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

resource "aws_s3_bucket" "example" {
  bucket = "iac-serverce1"

  tags = {
    Name        = "iac-service1"
    Environment = "Dev"
    Management  = "Terraform"
    CreatedBy   = "hwan001"
  }
}

 

728x90
반응형
728x90
반응형

Git과 PR(Pull Request)을 활용한 협업

 인프라의 규모가 확장되면서 여러 팀원들과 함께 작업하는 상황에서는 동일한 상태 파일을 공유하고 동기화하는 것이 중요해졌다.

 

https://kschoi728.tistory.com/139

 

유형 1은 직접 .tf 파일과 tfstate 파일을 공유하는 방식이다. 이런 방식으로 파일을 공유할 수는 있지만, 수동으로 관리하는 데에는 여러 번거로운 작업들이 포함된다. 이 과정에서 인간의 실수나 정보의 누락이 발생할 수 있기 때문에 이러한 문제점을 해결하고자 버전 관리 시스템(VCS)의 도입이 필요해졌다.

유형 2에서는 VCS(예: git, svn)를 사용하여 해당 파일들을 효율적으로 관리한다. 이를 통해 변경 이력을 체계적으로 추적할 수 있으며, 필요한 경우 이전 버전으로 쉽게 롤백할 수 있게 되었다.

유형 3은 git을 사용하여 .tf 파일을 관리하는 동시에, s3와 같은 백엔드 서비스를 활용하여 tfstate 파일을 안전하게 저장하고 관리하는 방식이다. 이 구조를 사용하면 각 파일의 특성에 맞는 최적화된 관리 방식을 선택할 수 있다.

 

 일반적으로는 유형 3의 구조에서 Git 저장소에 브랜치를 생성한 후 PR을 보내서 작업을 진행하게 된다.

PR은 git으로 코드를 관리할 때 주로 사용되는 브랜치 branch를 이용하는 방법으로 작업자가 코드 수정을 위해 새로운 브랜치를 생성하고 작업한 후 본인의 브랜치를 push하여 코드리뷰 후 Merge를 요청하는 방식이다.

Push와 Pull만으로도 코드 협업이 가능하긴 하지만 이렇게 할 경우, 다른 사람이 작성한 커밋을 확인하기가 쉽지 않고 리모트 저장소에 푸시할 때가 되어야 충돌 상황을 확인 할 수 있다.
PR을 사용하면 작업된 테라폼 코드에 대한 자연스러운 리뷰와 메인스트림 main branch의 병합을 관리하기 수월해진다.

 

이전에 만들었던 Terraform VPC Module에 PR을 보내기 위해 feature 브랜치를 생성했다.

브랜치 생성

작업 후에 PR을 생성하고 main 브랜치에 Merge 해보자.

commit 후 push
open a pull request
comment
merge

 

위 과정을 거치면 다른 브랜치를 통해서 작업한 내용을 main브랜치에 합칠 수 있다.

 


Hashcorp TFC (Terraform Cloud) 백엔드

  위에서 VCS와 PR을 활용하는 방법을 확인했다. 하지만 유형3에서 코드의 관리만큼 중요한 상태 파일의 관리를 해결하지 못했는데,

s3 등의 시스템들을 활용하면 백엔드로 관리하면 상태 파일을 안정적으로 공유할 수 있다. 

그 중 TFC 백엔드에 대해서 정리해보려고 한다.

 

하시코프에서 프로비저닝 대상과 별개로 State를 관리할 수 있도록 SaaS 환경인 TFC를 무상 제공한다.

TFC는 아래 링크로 접속할 수있다. 

https://app.terraform.io/session

 

Terraform Cloud by HashiCorp

 

app.terraform.io

 

먼저, email로 계정을 생성한 뒤 로그인하여 Organizations를 생성한다.

최초 화면

 

hwan001-test-org라는 이름으로 Organizations를 생성했다.

org 생성

 

이제 아래의 코드로 테라폼 파일을 만들어주자.

terraform{
  cloud{
    organization = "hwan001-test-org"      # 생성한 ORG 이름 지정
    hostname     = "app.terraform.io"      # default

    workspaces {
      name = "terraform-aws-collaboration"  # 없으면 생성됨
    }
  }
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 4.0"
    }
  }
}

provider "aws" {
  region  = "ap-northeast-2"
  # shared_config_files      = ["~/.aws/config"]
  # shared_credentials_files = ["~/.aws/credentials"]
}

module "vpc" {
  source = "git::https://github.com/aws-hwan-001/vpc.git"

  cidr_block             = "10.10.0.0/16"
  tags                   = { Name = "hwan001-vpc" }
  public_subnet_cidr    = "10.10.1.0/24"
  public_subnet_tags    = { Name = "hwan001-public-subnet" }
  private_subnet_cidr    = "10.10.2.0/24"
  private_subnet_tags    = { Name = "hwan001-private-subnet" }
}

 

terraform init을 실행하면 login이 필요하다는 경고가 나온다.

terraform login 명령어를 실행하고 yes를 입력하면 아래처럼 토큰을 생성하는 페이지가 생성(웹에 로그인되어 있을 경우)된다.

토큰생성 페이지

해당 페이지에서 토큰을 생성한 후 복사하고, shell에 입력해주면 아래와 같은 화면이 나오면서 login 된다.

terraform login

 

로그인 후 다시 terraform init을 진행하면 정상적으로 진행되면서 웹UI에서 새로운 프로젝트가 생긴걸 볼 수 있다.

 

배포를 원하는 AWS 계정의 Credential  정보를 아래 경로에 Variable Sets으로 설정해주자.

Settings > Variable Sets

 

이 상태에서 Terraform plan과 apply가 아래처럼 성공하면 아래처럼 state를 TFC에서 확인할 수 있다.

728x90
반응형
728x90
반응형

State  file

 상태 파일은 terraform apply시에 생성되는 .tfstate 파일이다.

해당 파일은 인프라의 상태를 json 형태로 가지고 있는데, 테라폼 내부에서만 사용하기 위한 파일이기 때문에 테라폼 상태 파일을 직접 편집하거나 직접 읽는 코드로 작성해서는 안된다.

 

팀 단위로 작업하는 경우 아래 조건이 갖춰져야 한다.

  1. 상태 파일을 저장하는 공유 스토리지 (Shared storage for state files)
    • 각 팀원이 동일한 테라폼 상태 파일 사용을 위해서, 공유 위치에 저장이 필요
  2. 상태 파일 잠금 (Locking state files)
    • 잠금 기능 없이 동시에 테라폼 실행 시 여러 테라폼 프로세스가 상태 파일을 동시에 업데이트하여 경쟁 상태(race condition)로 인한 충돌 가능
  3. 상태 파일 격리 (Isolating state files)
    • 테스트(dev), 검증(stage), 상용(prodction) 등 각 환경에 대한 상태 파일의 격리가 필요

 

AWS S3, Azure Blob Storage, Google Cloud Storage 등이 원격 백엔드를 지원하기 때문에 많이 쓰인다.

이런 백엔드를 사용할 경우 일반적으로 발생하는 아래 문제들을 해결할 수 있다.

  1. 수동 오류: plan/apply 실행 시 마다 해당 백엔드에서 파일을 자동을 로드, apply 후 상태 파일을 백엔드에 자동 저장
  2. 잠금: apply 실행 시 테라폼은 자동으로 잠금을 활성화, -lock-timout=<TIME> 로 대기 시간 설정 지정 가능
  3. 보안: 대부분 원격 백엔드는 기본적으로 데이터를 보내거나 상태 파일을 저장할 때 암호화 기능을 지원

 

아래 코드를 사용하면 DynamoDB로 잠금 상태를 제어하는 AWS S3 Backend를 만들수 있다.

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

resource "aws_s3_bucket" "mys3bucket" {
  bucket = "hwan001-t101study-tfstate"
}

resource "aws_s3_bucket_versioning" "mys3bucket_versioning" {
  bucket = aws_s3_bucket.mys3bucket.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_dynamodb_table" "mydynamodbtable" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

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

S3 bucket 생성
DynamoDB 생성

 

만들어진 백엔드와 DynamoDB 잠금은 아래처럼 사용할 수 있다.

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

terraform {
  backend "s3" {
    bucket = "hwan001-t101study-tfstate"
    key    = "dev/terraform.tfstate"
    region = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    # encrypt        = true
  }
}

Module

 모듈은 대부분의 프로그래밍 언어에서 쓰이는 라이브러리나 패키지와 역할이 비슷하다.

모듈은 기본적으로 아래와 같은 구조를 가지며 이렇게 모듈화를 할 경우 재활용성이 높아진다.

 

Root-Module과 Child-Module로 나누어지고, root 모듈 내부에서  child 모듈을 불러서 사용할 수 있다.

모듈을 사용하는 방법은 로컬 디렉토리, 테라폼 레지스트리와 github에 있는 모듈 사용 등이 있다.

 

아래는 깃허브를 사용한 방식으로 ec2 인스턴스를 띄워봤다.

먼저 github에 public 리포지토리를 생성한다. 생성 시 .gitignore에서 Terraform을 선택하면 .terraform 등의 과정에서 생성된 파일들이 github push 시에 올라가지 않는다.

 

VPC Module

 

GitHub - aws-hwan-001/vpc

Contribute to aws-hwan-001/vpc development by creating an account on GitHub.

github.com

data "aws_availability_zones" "available" {
  state = "available"
}
# variables.tf
variable "cidr_block" {
  description = "The CIDR block for the VPC"
  type        = string
}

variable "tags" {
  description = "Tags for the VPC"
  type        = map(string)
  default     = {}
}

variable "primary_subnet_cidr" {
  description = "CIDR block for the primary subnet"
  type        = string
}

variable "primary_subnet_tags" {
  description = "Tags for the primary subnet"
  type        = map(string)
  default     = {}
}

variable "secondary_subnet_cidr" {
  description = "CIDR block for the secondary subnet"
  type        = string
}

variable "secondary_subnet_tags" {
  description = "Tags for the secondary subnet"
  type        = map(string)
  default     = {}
}


# main.tf
resource "aws_vpc" "this" {
  cidr_block = var.cidr_block
  tags       = var.tags
}

resource "aws_subnet" "primary" {
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.primary_subnet_cidr
  availability_zone = data.aws_availability_zones.available.names[0]
  tags              = var.primary_subnet_tags
}

resource "aws_subnet" "secondary" {
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.secondary_subnet_cidr
  availability_zone = data.aws_availability_zones.available.names[1]
  tags              = var.secondary_subnet_tags
}

# output.tf
output "vpc_id" {
  description = "The ID of the VPC"
  value       = aws_vpc.this.id
}

output "primary_subnet_id" {
  description = "The ID of the primary subnet"
  value       = aws_subnet.primary.id
}

output "secondary_subnet_id" {
  description = "The ID of the secondary subnet"
  value       = aws_subnet.secondary.id
}

 

Security Group Module

 

GitHub - aws-hwan-001/sg

Contribute to aws-hwan-001/sg development by creating an account on GitHub.

github.com

# variables.tf
variable "description" {
  description = "Description of the security group"
  type        = string
}

variable "vpc_id" {
  description = "VPC ID to associate the security group with"
  type        = string
}

variable "ingress_rules" {
  description = "List of ingress rules"
  type        = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
}

variable "name" {
  description = "Name of the security group"
  type        = string
}

# main.tf
resource "aws_security_group" "this" {
  description = var.description
  vpc_id      = var.vpc_id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }

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

  tags = {
    Name = var.name
  }
}

# output.tf
output "security_group_id" {
  description = "The ID of the security group"
  value       = aws_security_group.this.id
}

 

 

EC2 Module

 

GitHub - aws-hwan-001/ec2

Contribute to aws-hwan-001/ec2 development by creating an account on GitHub.

github.com

# variables.tf
variable "instance_type" {
  description = "EC2 instance type"
  type        = string
}

variable "subnet_id" {
  description = "Subnet id to launch the instance in"
  type        = string
}

variable "security_group_ids" {
  description = "List of security group ids to associate with the instance"
  type        = list(string)
}

variable "instance_name" {
  description = "Name to be used on all resources as prefix"
  type        = string
}

variable "associate_public_ip_address" {
  description = "Associate a public ip address with the instance"
  type        = bool
  default     = false
}

# main.tf
data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]  # Canonical
}

resource "aws_instance" "this" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type
  subnet_id     = var.subnet_id
  vpc_security_group_ids = var.security_group_ids
  associate_public_ip_address = var.associate_public_ip_address

  tags = {
    Name = var.instance_name
  }
}

# output.tf
output "instance_id" {
  description = "The ID of the instance"
  value       = aws_instance.this.id
}

 

이제 만들어진 모듈을 사용해 리소스를 생성해보자.

ec2 모듈에 count = 3을 넣어주어 3개의 인스턴스를 생성했다.

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

terraform {
  backend "s3" {
    bucket = "hwan001-t101study-tfstate"
    key    = "dev/terraform.tfstate"
    region = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    # encrypt        = true
  }
}


module "vpc" {
  source = "git::https://github.com/aws-hwan-001/vpc.git"

  cidr_block             = "10.10.0.0/16"
  tags                   = { Name = "hwan001-vpc" }
  primary_subnet_cidr    = "10.10.1.0/24"
  primary_subnet_tags    = { Name = "hwan001-subnet-1" }
  secondary_subnet_cidr  = "10.10.2.0/24"
  secondary_subnet_tags  = { Name = "hwan001-subnet-2" }
}


module "security_group" {
  source  = "git::https://github.com/aws-hwan-001/sg.git"
  
  description = "hwan001 Security Group"
  vpc_id      = module.vpc.vpc_id
  name        = "hwan001-sg"
  ingress_rules = [
    {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 8080
      to_port     = 8080
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  ]
}


module "ec2_instance" {
  source  = "git::https://github.com/aws-hwan-001/ec2.git"
  count = 3
  
  instance_type = "t2.micro"
  subnet_id     = module.vpc.primary_subnet_id
  security_group_ids = [module.security_group.security_group_id]
  instance_name = "hwan001-ec2"
  associate_public_ip_address = true
}

 

상태파일은 위에서 만든 백엔드를 사용하고 있는 걸 볼 수 있다.

 


 

728x90
반응형
728x90
반응형

조건문

 Terraform에는 다른 언어처럼 조건문을 위한 if 키워드가 없기 때문에 3항 연산자를 사용하여 조건문을 작성해야 한다.

예를들면 아래처럼 환경에 대한 정보를 변수로 입력받아 product 환경일 경우 5번, 아닐 경우 1번만 리소스를 생성하도록 할 수 있다.

resource "aws_instance" "hwan001_instance" {
  count = var.env == "prod" ? 5 : 1
}

 

만약 dev 환경일 경우에만 리소스를 생성하고 싶다면 아래처럼 작성할 수 있다.

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

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]  # Canonical
}

resource "aws_instance" "hwan001-ec2-dev" {
  count				= var.env == "dev" ? 1 : 0
  ami				= data.aws_ami.ubuntu.id
  instance_type			= "t2.micro"
  subnet_id			= aws_subnet.primary_az.id
  vpc_security_group_ids	= [aws_security_group.hwan001-sg.id]
  associate_public_ip_address	= true

  tags = {
    Name = "hwan001-ec2"
    env  = var.env
  }
}

resource "aws_instance" "hwan001-ec2-prod" {
  count				= var.env == "prod" ? 3 : 0
  ami				= data.aws_ami.ubuntu.id
  instance_type			= "t2.micro"
  subnet_id			= aws_subnet.primary_az.id
  vpc_security_group_ids	= [aws_security_group.hwan001-sg.id]
  associate_public_ip_address	= true

  tags = {
    Name = "hwan001-ec2"
    env  = var.env
  }
}

 


내장함수

 내장 함수는 테라폼에 내장되어 있는 함수들로 여러 상황에서 유용하게 사용될 수 있으며, 아래와 같은 여러 이점들이 있다. 

  • 간결성 : 코드를 훨씬 간결하고 읽기 쉽게 만들 수 있다. 
  • 확장성 : 인프라 구조를 보다 쉽게 확장하거나 수정할 수 있다.
  • 유연성 : 다양한 데이터 타입(문자열, 숫자, 목록, 맵 등)을 처리하는데 유연성을 제공합니다.
  • 유효성 검사 : 내장 함수를 사용하면 입력값의 유효성을 쉽게 검사할 수 있다.
  • 기능성 : file이나 timestamp같은 일부 내장 함수는 특정 기능을 제공합니다.

 

아래는 많이 사용하는 내장 함수들이다. 

file : 입력받은 파일의 경로를 읽어 반환한다.

resource "aws_iam_policy" "iam_policy" {
  name        = "iam_policy"
  path        = "/"

  policy = file("policy.json")
}

 

format, formatlist : 문자열의 포맷을 정의 할 수 있다.

output "instance_id" {
  value = format("Instance ID is %s", aws_instance.hwan001-ec2.id)
}

 

element : 주어진 목록에서 특정 요소를 가져온다.

output "first_subnet" {
  value = element(aws_subnet.hwan001-subnet.*.id, 0)
}

 

lookup : 주어진 map 에서 특정 값을 찾는다.

output "instance_public_ip" {
  value = lookup(aws_instance.hwan001-ec2.*.attributes, "public_ip")
}

 

count.index : 현재 인덱스의 번호를 반환하며, 보통 반복적으로 리소스를 사용할 때 count와 같이 사용한다.

resource "aws_instance" "hwan001-ec2" {
  count         = 3
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  tags = {
    Name = "hwan001-ec2-instance-${count.index}"
  }
}

 

split, join : 문자열을 특정 구분자로 분리하거나 합칠 때 사용한다.

output "subnet_ids" {
  value = join(", ", aws_subnet.hwan001-subnet.*.id)
}

 

cidrhost :  네트워킹 설정을 할 때 유용하다. 예를 들면 여러 서브넷이 있는 VPC를 설정할 때, 각 서브넷의 IP 범위를 계산하는데 사용할 수 있다.

output "subnet_ip" {
  value = cidrhost("10.0.0.0/16", 4)
}

위 코드처럼 사용할 경우 10.0.0.0/16 의 4번째 호스트의 IP 주소(10.0.0.4)를 반환한다.

 

테라폼에서는 사용자가 함수를 직접 정의할 수는 없지만 다양한 함수들을 제공한다. 아래 문서에서 더 많은 정보를 얻을 수 있다.

https://developer.hashicorp.com/terraform/language/functions

 

Functions - Configuration Language | Terraform | HashiCorp Developer

An introduction to the built-in functions that you can use to transform and combine values in expressions.

developer.hashicorp.com


프로비저너

 프로바이더에서 제공되지 않는 파일 복사, 커맨드 등의 역할을 수행할 수 있지만 해당 코드로 인한 결과는 tfstate에 동기화되지 않는다.

따라서 실행 시 마다 실행의 결과가 항상 동일하지 않을 수도 있다.

 예를 들어 local-exec 프로비저너를 사용하면 테라폼을 활용해 리소스를 프로비저닝한 후 해당 리소스 내부를 앤서블 플레이북으로 프로비저닝 하는 등의 작업이 가능하다. 

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

resource "aws_instance" "hwan001-ec2" {
  ami           = "ami-0c94855ba95c574c8"
  instance_type = "t2.micro"

  provisioner "local-exec" {
    command = "ansible-playbook -u ubuntu --private-key=${var.private_key_path} -i '${self.public_ip},' playbook.yml"
  }
}

 

하지만 해당 방식은 ssh 접속을 위한 정보가 필요하고, 여러 변수로 인해 안정적으로 프로비저닝되지 않을 수도 있다.

테라폼에서는 최근부터 terraform-provider-ansible를 제공한다. 

해당 프로바이더를 사용하면 tfstate 파일을 사용하여 인프라를 추적할 수 있기 때문에 좀 더 안정적으로 앤서블을 사용할 수 있다.

terraform {
  required_providers {
    ansible = {
      source = "ansible/ansible"
      version = "1.1.0"
    }
  }
}

provider "ansible" {}

resource "ansible_playbook" "playbook" {
  playbook   = "playbook.yml"
  name       = "host-1.example.com"
  replayable = true

  extra_vars = {
    var_a = "Some variable"
    var_b = "Another variable"
  }
}

 

자세한 정보는 아래링크에서 확인할 수 있다.

https://registry.terraform.io/providers/ansible/ansible/latest/docs

 

Terraform Registry

 

registry.terraform.io

https://github.com/ansible/terraform-provider-ansible/tree/main

 

GitHub - ansible/terraform-provider-ansible: community terraform provider for ansible

community terraform provider for ansible. Contribute to ansible/terraform-provider-ansible development by creating an account on GitHub.

github.com

 


Null Resource와 Terraform data

Null Resource

 Null Resource는 아무런 액션도 취하지 않는 리소스로 그 자체로는 아무런 효과가 없지만, 특정 조건에 따라 다른 리소스를 트리거하는 등의 복잡한 의존성이 필요한 경우에 유용하게 사용할 수 있다.

예를 들면 특정 조건에 따라 스크립트를 실행하거나, 다른 리소스가 변경될 때마다 새로운 리소스를 생성하는 등의 작업에 사용할 수 있다.

resource "null_resource" "example" {
  triggers = {
    always_run = "${timestamp()}"
  }

  provisioner "local-exec" {
    command = "echo Hello, World!"
  }
}

 

Null Resource는 주로 아래의 경우에 사용된다.

  • 프로비저닝 수행 과정에서 명령어 실행
  • 프로비저너와 함께 사용
  • 모듈, 반복문, 데이터 소스, 로컬 변수와 함께 사용
  • 출력을 위한 데이터 가공

 

예를 들어, AWS EC2 인스턴스를 프로비저닝하면서 웹서비스를 실행시키고 싶지만, 웹서비스 설정에는 노출되어야 하는 고정된 외부 IP가 포함된 구성이 필요하기 때문에 aws_eip 리소스를 생성해야 한다.

 

이런 경우 아래처럼 코드를 작성하면 aws_eip 부분에선 aws_instance.example.id가 필요하고, aws_instance 에선 aws_eip.myeip.public_ip가 필요하기 때문에 Cycle이 발생한다.

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

resource "aws_security_group" "instance" {
  name = "hwan001-sg"

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

resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"
  subnet_id              = "subnet-000000" 
  private_ip             = "172.31.1.100"
  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "Test page" > index.html
              nohup busybox httpd -f -p 80 &
              EOF

  tags = {
    Name = "Single-WebSrv"
  }

  provisioner "remote-exec" {
    inline = [
      "echo ${aws_eip.myeip.public_ip}"
     ]
  }
}

resource "aws_eip" "myeip" {
  instance = aws_instance.example.id
  associate_with_private_ip = "172.31.1.100"
}

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

 

이런 경우 Null Resource를 사용해서 2개의 리소스 생성에 시간 간격을 두어 해결할 수도 있다.

...

resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"
  subnet_id              = "subnet-000000" 
  private_ip             = "172.31.1.100"
  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "Test page" > index.html
              nohup busybox httpd -f -p 80 &
              EOF

  tags = {
    Name = "Single-WebSrv"
  }
}

resource "aws_eip" "myeip" {
  instance = aws_instance.example.id
  associate_with_private_ip = "172.31.1.100"
}

resource "null_resource" "foo" {
  triggers = {
    ec2_id = aws_instance.example.id # instance의 id가 변경되는 경우 재실행
  }

  provisioner "remote-exec" {
    inline = [
      "echo ${aws_eip.myeip.public_ip}"
     ]
  }
}

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

 

Terraform_data

  Terraform 1.4버전 부터는 Null Resource를 대체하기 위해 terraform_data라는 리소스를 지원한다.

Null Resource를 사용하려면 null 공급자를 추가해줘야 한다. 하지만 Terraform Data는 해당 공급자를 추가하지 않아도 동작한다.

resource "terraform_data" "apply" {
  provisioner "local-exec" {
    command     = "echo ${aws_eip.myeip.public_ip}"
  }

  triggers_replace = [
    aws_instance.example.id
  ]
}

terraform_data 사용 시
null_resource 사용 시

좀 더 자세한 정보는 아래 링크 참고

https://developer.hashicorp.com/terraform/language/resources/terraform-data

 

The terraform_data Managed Resource Type | Terraform | HashiCorp Developer

Retrieves the root module output values from a Terraform state snapshot stored in a remote backend.

developer.hashicorp.com

 


moved 블록

 테라폼의 tfstate에서 리소스 주소의 이름이 변경되면 기존 리소스는 삭제되고 새로운 리소스가 생성된다. 하지만 기존 리소스의 이름만 변경하고 싶은 경우가 있다.

리소스의 이름은 변경되지만 이미 테라폼으로 프로비저닝된 환경을 그대로 유지하려고 하는 경우엔 moved 블록을 사용할 수 있다. (Terraform 1.1 이상부터 지원)

 

아래 코드로 ec2 instance를 생성했다.

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

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]  # Canonical
}

# EC2 Instance 생성
resource "aws_instance" "hwan001-ec2" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  subnet_id                   = aws_subnet.primary_az.id
  vpc_security_group_ids      = [aws_security_group.hwan001-sg.id]
  associate_public_ip_address = true

  tags = {
    Name = "hwan001-ec2"
  }
}

 

해당 리소스 태그를 moved로 변경해보자.

resource "aws_instance" "hwan001-ec2-new" {
  ami				= data.aws_ami.ubuntu.id
  instance_type			= "t2.micro"
  subnet_id			= aws_subnet.primary_az.id
  vpc_security_group_ids	= [aws_security_group.hwan001-sg.id]
  associate_public_ip_address	= true

  tags = {
    Name = "hwan001-ec2-new"
  }
}

moved {
  from = aws_instance.hwan001-ec2
  to   = aws_instance.hwan001-ec2-new
}

moved를 사용해서 리소스의 삭제없이 인스턴스의 태그 정보를 변경했다. 

해당 작업 후 moved 부분을 제거해주면 된다.

 

(cc. moved를 사용해도 associate_public_ip_address이나 instance_type등을 변경하고 apply하면 리소스가 재생성된다.)

 


CLI를 위한 시스템 환경 변수

 Terraform에서는 환경 변수를 사용하여 다양한 설정을 구성할 수 있다.

  • TF_LOG : 로그 레벨 (TRACE, DEBUG, INFO, WARN, ERROR)
  • TF_LOG_PATH : 로그 파일의 경로, TF_LOG 환경 변수가 설정되어 있고 TF_LOG_PATH가 설정되어 있으면 로그를 기록한다.
  • TF_VAR_name : Terraform 변수의 값 (name은 설정하려는 변수의 이름, ex) TF_VAR_instance_type=t2.micro)
  • TF_INPUT : 사용자 입력 요청 여부를 제어, false일 경우 사용자 입력을 요청 안함
  • TF_IN_AUTOMATION : 자동화 도구 내에서 실행되고 있는지 여부, 이 환경 변수가 설정되어 있으면 자동화 도구와의 호환성을 향상시킨다.
  • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN : AWS 프로바이더 인증 키
  • TF_CLI_ARGS / TF_CLI_ARGS_subcommand : 테라폼 실행 시 추가할 인수를 정의
  • TF_DATA_DIR : State 저장 백엔드 설정과 같은 작업 디렉터리별 데이터를 보관하는 위치를 지정

https://developer.hashicorp.com/terraform/cli/config/environment-variables

 

Environment Variables | Terraform | HashiCorp Developer

Learn to use environment variables to change Terraform's default behavior. Configure log content and output, set variables, and more.

developer.hashicorp.com

 


프로바이더

 테라폼은 terraform 바이너리 파일을 시작으로 로컬 환경에나 배포 서버와 같은 원격 환경에서 원하는 대상(프로바이더가 제공하는 API)을 호출하는 방식으로 실행된다. 각 프로바이더의 API 구현은 서로 다르지만 테라폼의 고유 문법으로 동일한 동작을 수행하도록 구현되어 있다.

https://malwareanalysis.tistory.com/619

 

다수의 프로바이더를 로컬 이름 지정

 required_providers 블록을 사용하여 다수의 프로바이더를 로컬 이름으로 지정해 사용할 수 있다.

예를 들면 아래는 여러 프로바이더가 제공해주는 동일한 http 관련 data 블록을 사용하는 예제이다.

terraform {
  required_providers {
    http = {
      source = "hashicorp/http"
      version = "3.4.0"
    }
    aws-http = {
      source = "terraform-aws-modules/http"
      version = "2.4.1"
    }
  }
}

data "http" "example1" {
  provider = http
  url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"

  request_headers = {
    Accept = "application/json"
  }
}

data "http" "example2" {
  provider = http
  url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"

  request_headers = {
    Accept = "application/json"
  }
}

 

해당 코드를 테라폼으로 apply하면 tfstate파일 내에서 해당 요청을 서로 다른 공급자로 얻어오는 걸 볼 수 있다. 

{
  ...
  "resources": [
    {
      "mode": "data",
      "type": "http",
      "name": "example1",
      "provider": "provider[\"registry.terraform.io/hashicorp/http\"]",
      "instances": [ ... ]
    },
    {
      "mode": "data",
      "type": "http",
      "name": "example2",
      "provider": "provider[\"registry.terraform.io/terraform-aws-modules/http\"]",
      "instances": [ ... ]
    }
  ],
  ...
}

 

단일 프로바이더의 다중 정의

 위의 경우와는 반대로 하나의 프로바이더를 여러번 정의해서 사용해야 하는 경우가 있다.

예를 들면 멀티 리전을 정의하는 경우인데, 아래처럼 alias를 활용해 같은 리소스 내부에서 리전을 지정해 줄 수 있다.

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

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

resource "aws_instance" "app_server1" {
  ami           = "ami-06b79cf2aee0d5c92"
  instance_type = "t2.micro"
}

resource "aws_instance" "app_server2" {
  provider      = aws.seoul
  ami           = "ami-0ea4d4b8dc1e46212"
  instance_type = "t2.micro"
}

 

하지만 해당 방식은 지역간 지연 시간, 고유 ID, 최종 일관성 등 여러가지 고려사항이 많고, 한 리전이 다운되었을 경우 전체 plan과 apply가 실패하기 때문에 다른 리전의 변경 사항도 적용할 수 없게 된다.

프로덕션 수준에서는 환경을 완전히 격리하여 서로 간의 영향도를 최소화하는 방법이 더 좋을 것 같다.


 

 

728x90
반응형
728x90
반응형

데이터 소스

 Terraform의 데이터 소스는 테라폼 설정 내에서 읽기 전용의 값들을 제공한다. 이 값을 활용하면 이미 존재하는 리소스의 세부사항이나 외부 데이터를 참조할 수 있다.

데이터 소스 블록은 아래처럼 data로 시작하고 ‘데이터 소스 유형’과 '이름'을 정의한다.

data "<리소스 유형>" "<이름>" {
  <인수> = <값>
}

# 참조 : data.<리소스 유형>.<이름>.<속성>
# 사용가능한 메타인수:
# depends_on : 종속성 선언
# count : 선언된 개수만큼 리소스를 생성
# for_each : map또는 set 타입 변수로 리소스 반복 생성
# lifecycle : 리소스 수명주기

 

예를 들면 아래 코드는 aws의 az 정보를 얻어와 각 az에 subnet을 생성할 수 있다.

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_subnet" "primary_AZ" {
  availability_zone = data.aws_availability_zones.available.names[0]
}

resource "aws_subnet" "secondary_AZ" {
  availability_zone = data.aws_availability_zones.available.names[1]
}

 

연습 코드 : 

  • data 모듈로 az 이름 받아와서 vpc, subnet 생성하기
data "aws_availability_zones" "available" {
  state = "available"
}


resource "aws_vpc" "hwan001-vpc" {
    cidr_block = "10.10.0.0/16"

    tags = {
        Name = "hwan001-vpc"
    }
}

resource "aws_subnet" "primary_az" {
    vpc_id = aws_vpc.hwan001-vpc.id
    cidr_block =  "10.10.1.0/24"
    availability_zone = data.aws_availability_zones.available.names[0]
    tags = {
        Name = "hwan001-subnet-1"
    }
}

resource "aws_subnet" "secondary_az" {
    vpc_id = aws_vpc.hwan001-vpc.id
    cidr_block =  "10.10.2.0/24"
    availability_zone = data.aws_availability_zones.available.names[1]
    tags = {
        Name = "hwan001-subnet-2"
    }
}

 


변수(Variables) 종류 및 우선순위

변수 종류 : 

 변수 종류는 크게 기본 유형과 집합 유형으로 나뉜다.

기본 유형에는 string, number, bool, any가 있고, 집합 유형에는 list, map, set, object, tuple이 있다.

변수 정의할 땐 아래 메타인수가 사용 가능하다.

  • default : 값이 전달되지 않은 경우의 기본 값, default가 없으면 apply 시 변수에 대한 정보를 물어봄
  • type : 변수 유형 정의, string number bool list map set object tuple 와 유형을 지정하지 않으면 any 유형으로 간주
  • description : 입력 변수의 설명
  • validation : 변수 선언의 제약조건을 추가해 유효성 검사 규칙을 정의
  • sensitive : 민감한 변수 값으로 테라폼의 출력문에서 값 노출을 제한함 (암호 등 민감 데이터의 경우)
  • nullable : 변수에 값이 없어도 됨 (null 허용 여부)

사용 예시는 아래와 같다.

variable "string" {
  type        = string
  description = "var String"
  default     = "myString"
}

variable "number" {
  type    = number
  default = 123
}

variable "boolean" {
  default = true
}

variable "list" {
  default = [
    "google",
    "vmware",
    "amazon",
    "microsoft"
  ]
}

output "list_index_0" {
  value = var.list.0 # google, 1이면 vmware
}

output "list_all" {
  value = [
    for name in var.list : upper(name)
  ]
}

variable "map" { # Sorting
  default = {
    aws   = "amazon",
    azure = "microsoft",
    gcp   = "google"
  }
}

variable "set" { # Sorting
  type = set(string)
  default = [
    "google",
    "vmware",
    "amazon",
    "microsoft"
  ]
}

variable "object" {
  type = object({ name = string, age = number })
  default = {
    name = "abc"
    age  = 12
  }
}

variable "tuple" {
  type    = tuple([string, number, bool])
  default = ["abc", 123, true]
}

variable "ingress_rules" { # optional ( >= terraform 1.3.0)
  type = list(object({
    port        = number,
    description = optional(string),
    protocol    = optional(string, "tcp"),
  }))
  default = [
    { port = 80, description = "web" },
  { port = 53, protocol = "udp" }]
}

variable "my_password" {
  default   = "password"
  sensitive = true
}

 

우선 순위 : 

 변수가 선언되는 방식에 따라 우선순위가 존재한다.

실행 후 입력 < variable 블록의 default 값 < 환경 변수 (TF_VAR 변수 이름) < terraform.tfvars < *.auto.tfvars < *.auto.tfvars.json < CLI 실행 시 -var 인수에 지정 또는 -var-file로 파일 지정

오른쪽으로 갈수록 우선순위가 높다. 예를들어 변수의 값을 출력해주는 코드가 있을 때, terraform.tfvars 내부 변수에 1이라는 값이 정의되어 있고, 실행후에 3이라는 값을 입력 한다면 결과는 1이 나오게 된다.

# main.tf 
variable "my_var" {
  default = "var2"
}

resource "local_file" "abc" {
  content  = var.my_var
  filename = "${path.module}/abc.txt"
}

#terraform.tf
my_var="var4"


기본 문법 

local : 로컬에서만 사용하는 변수

output : 프로비저닝 수행 결과의 속성 값을 출력

반복문 : count, for each, for expression 등 여러개의 데이터를 한개의 리소스로 반복적으로 생성

# count
resource "local_file" "abc" {
  count    = 5
  content  = "abc"
  filename = "${path.module}/abc.txt"
}


# for each
resource "local_file" "abc" {
  for_each = {
    a = "content a"
    b = "content b"
  }
  content  = each.value
  filename = "${path.module}/${each.key}.txt"
}


# for
variable "names" {
  default = ["a", "b", "c"]
}

resource "local_file" "abc" {
  content  = jsonencode([for s in var.names : upper(s)]) # 결과 : ["A", "B", "C"]
  filename = "${path.module}/abc.txt"
}

# for의 다양한 활용
variable "names" {
  type    = list(string)
  default = ["a", "b"]
}

output "A_upper_value" {
  value = [for v in var.names : upper(v)]
}

output "B_index_and_value" {
  value = [for i, v in var.names : "${i} is ${v}"]
}

output "C_make_object" {
  value = { for v in var.names : v => upper(v) }
}

output "D_with_filter" {
  value = [for v in var.names : upper(v) if v != "a"]
}

 

Dynamic : 리소스 내부 속성 블록 동적인 블록으로 생성한다. dynamic을 사용할 경우 특정 리소스 블록 내부에서 하나의 속성 블록에서 여러 개의 속성을 부여할 수 있게 된다.

# 일반적인 속성 부여
resource "provider_resource" "name" {
  name = "some_resource"

  some_setting {
    key = a_value
  }

  some_setting {
    key = b_value
  }

  some_setting {
    key = c_value
  }

  some_setting {
    key = d_value
  }
}

# dynamic을 활용한 부여
resource "provider_resource" "name" {
  name = "some_resource"

  dynamic "some_setting" {
    for_each = {
      a_key = a_value
      b_key = b_value
      c_key = c_value
      d_key = d_value
    }

    content {
      key = some_setting.value
    }
  }
}

 

연습 코드 :

  • 입력받은 수 만큼 count로 sqs에 큐 생성하기
provider "aws" {
  region = "ap-northeast-2" 
}

variable "number_of_queue" {
    type = number
}

resource "aws_sqs_queue" "main_queue" {
    count                     = var.number_of_queue
    name                      = "queue-${count.index+1}"
    delay_seconds             = 0
    max_message_size          = 1024
    message_retention_seconds = 345600
    visibility_timeout_seconds = 60
}

 

  • dynamic을 활용한  security group 생성하기
variable "ingress_rules" {
    type = list(object(
        {
            from_port   = number
            to_port     = number
            protocol    = string
            cidr_blocks = list(string)
        }
    ))

    default = [
        {
            from_port   = 22
            to_port     = 22
            protocol    = "tcp"
            cidr_blocks = ["0.0.0.0/0"]
        },
        {
            from_port   = 80
            to_port     = 80
            protocol    = "tcp"
            cidr_blocks = ["0.0.0.0/0"]
        },
        {
            from_port   = 443
            to_port     = 443
            protocol    = "tcp"
            cidr_blocks = ["0.0.0.0/0"]
        },
        {
            from_port   = 8080
            to_port     = 8080
            protocol    = "tcp"
            cidr_blocks = ["0.0.0.0/0"]
        }
    ]
}

resource "aws_security_group" "hwan001-sg" {
    description = "hwan001 Security Group"
    vpc_id      = aws_vpc.hwan001-vpc.id
    
    dynamic "ingress" {
        for_each = var.ingress_rules
        content {
            from_port   = ingress.value.from_port
            to_port     = ingress.value.to_port
            protocol    = ingress.value.protocol
            cidr_blocks = ingress.value.cidr_blocks
        }
    }

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

    tags = {
        Name = "hwan001-sg"
    }
}

도전 과제

  • 리전 내에서 사용 가능한 가용영역 목록 가져오기를 사용한 VPC 리소스 생성 실습 진행
  • 위 3개 코드 파일 내용에 리소스의 이름(myvpc, mysubnet1 등)을 반드시! 꼭! 자신의 닉네임으로 변경해서 배포 실습해보세요!
  • 입력변수를 활용해서 리소스(어떤 리소스든지 상관없음)를 배포해보고, 해당 코드를 정리해주세요!
  • local를 활용해서 리소스(어떤 리소스든지 상관없음)를 배포해보고, 해당 코드를 정리해주세요!
  • count, for_each, for, dynamic문 을 활용해서 리소스(어떤 리소스든지 상관없음)를 배포해보고, 해당 코드를 정리해주세요!

 

network.tf :

  • data를 사용해 az 이름 가져오기
  • vpc, subnet 생성하기
data "aws_availability_zones" "available" {
  state = "available"
}


resource "aws_vpc" "hwan001-vpc" {
    cidr_block = "10.10.0.0/16"

    tags = {
        Name = "hwan001-vpc"
    }
}

resource "aws_subnet" "primary_az" {
    vpc_id = aws_vpc.hwan001-vpc.id
    cidr_block =  "10.10.1.0/24"
    availability_zone = data.aws_availability_zones.available.names[0]
    tags = {
        Name = "hwan001-subnet-1"
    }
}

resource "aws_subnet" "secondary_az" {
    vpc_id = aws_vpc.hwan001-vpc.id
    cidr_block =  "10.10.2.0/24"
    availability_zone = data.aws_availability_zones.available.names[1]
    tags = {
        Name = "hwan001-subnet-2"
    }
}

 

sg.tf : 

  • 다이나믹을 사용해서 여러 개의  ingress rule 정의하기
  • 보안 그룹 생성
variable "ingress_rules" {
    type = list(object(
        {
            from_port   = number
            to_port     = number
            protocol    = string
            cidr_blocks = list(string)
        }
    ))

    default = [
        {
            from_port   = 22
            to_port     = 22
            protocol    = "tcp"
            cidr_blocks = ["0.0.0.0/0"]
        },
        {
            from_port   = 80
            to_port     = 80
            protocol    = "tcp"
            cidr_blocks = ["0.0.0.0/0"]
        },
        {
            from_port   = 443
            to_port     = 443
            protocol    = "tcp"
            cidr_blocks = ["0.0.0.0/0"]
        },
        {
            from_port   = 8080
            to_port     = 8080
            protocol    = "tcp"
            cidr_blocks = ["0.0.0.0/0"]
        }
    ]
}

resource "aws_security_group" "hwan001-sg" {
    description = "hwan001 Security Group"
    vpc_id      = aws_vpc.hwan001-vpc.id
    
    dynamic "ingress" {
        for_each = var.ingress_rules
        content {
            from_port   = ingress.value.from_port
            to_port     = ingress.value.to_port
            protocol    = ingress.value.protocol
            cidr_blocks = ingress.value.cidr_blocks
        }
    }

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

    tags = {
        Name = "hwan001-sg"
    }
}

 

main.tf : 

  • ubuntu 20.04 공식 ami 정보를 data로 가져오기
  • 해당 ami를 사용해서 ec2 인스턴스 띄우기 (subnet, sg는 위에서 생성한 리소스로 사용)
provider "aws" {
  region = "ap-northeast-2" 
}

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]  # Canonical
}

# EC2 Instance 생성
resource "aws_instance" "hwan001-ec2" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  subnet_id                   = aws_subnet.primary_az.id
  vpc_security_group_ids      = [aws_security_group.hwan001-sg.id]
  associate_public_ip_address = true

  tags = {
    Name = "hwan001-ec2"
  }
}

 

결과 : 

생성된 서브넷
생성된 ec2
생성된 보안 그룹

728x90
반응형
728x90
반응형

Terraform

 HashiCorp사의 Terraform은 버전화, 재사용 및 공유할 수 있는 사람이 읽을 수 있는 구성 파일에서 클라우드 및 온프레미스 리소스를 모두 정의할 수 있는 코드형 인프라(IaC) 도구로 Amazon Web Services(AWS), Azure, Google Cloud Platform(GCP), Kubernetes, Helm, GitHub, Splunk, DataDog 등 수천여개의 다양한 Provider를 제공한다.

 

Provider

 테라폼을 적용할 대상으로 한번에 하나의 프로비저닝만 가능하며 terraform init을 통해 설치된다. (aws 구성을 azure로 변경하는 등의 작업은 불가함, 프로바이더에 대한 자세한 정보는 아래 링크 참조)

https://registry.terraform.io/browse/providers 

 

Terraform Registry

 

registry.terraform.io

원하는 프로바이터를 선택하고 오른쪽 상단의 USE PROVIDER 버튼을 누르면 간단한 사용방법이 나온다. 

 

Workflow and Terminology used in Terraform

 테라폼은 크게 write -> plan -> apply의 과정을 거쳐 인프라를 프로비저닝한다.

  • write : 여러 클라우드 공급자 및 서비스에 걸쳐 있을 수 있는 리소스를 정의한다. 
  • plan :존 인프라 및 구성(tfstate)을 기반으로 생성, 업데이트 또는 파괴할 인프라를 설명하는 실행 계획을 생성한다.
  • apply : 승인 시 Terraform은 모든 리소스 종속성을 고려하여 올바른 순서로 제안된 작업을 수행한다.

https://mindmajix.com/terraform-tutorial

 

 아래는 테라폼에서 사용하는 용어들이다.

  • resourece : 실제로 생성할 인프라 자원을 의미
  • output : 인프라를 프로비저닝 한 후에 생성된 자원을 output 부분으로 출력할 수 있음
  • backend : terraform의 상태를 저장할 공간 지정 (내부 / 외부 모두 가능 , 이 기능으로 다른사람들과 협업 가능)
  • module : 공통적으로 활용할 수 있는 인프라 코드를 한곳으로 모아 정의하는 부분 (변수만 바꿔서 동일한 리소스를 쉽게 생성 가능)
  • remote state : VPC , IAM 등과 같이 여러 서비스가 공통으로 사용하는 것이 가능 ( ftstate 파일이 저장돼 있는 backend 정보를 명시하면, trerraform이 backend에서 output 정보들을 가져옴 )
  • .tfstate 파일 : 테라폼이 인프라스트럭처의 상태를 추적하고 관리하는 핵심 도구로 아래와 같은 역할을 수행한다.
  1. 인프라스트럭처 상태 추적: 파일을 통해 프로비저닝된 리소스의 상태를 기록하고, 변경 사항을 추적하며, 이전 상태와의 차이를 분석하여 필요한 변경을 식별합니다. (plan 시 달라지는 부분을 확인할 수 있음)
  2. 변경 관리: 변경 사항을 .tfstate 파일에 반영하고 apply 시 이 파일을 사용하여 변경 사항을 적용한다. 이런 방식으로 인프라스트럭처를 안전하게 변경하고 일관성 있게 유지할 수 있다.
  3. 협업 및 공유: .tfstate 파일은 팀 간 협업과 공유를 지원한다. 팀원들은 동일한 .tfstate 파일(백엔드 등)에 액세스하여 상태를 공유하고, 변경 사항을 추적하고 충돌을 방지할 수 있다.
  4. 롤백 및 복구: .tfstate 파일은 변경 사항을 롤백하고 이전 상태로 복구하는 데 사용될 수 있다.

 


환경 구축

테라폼 설치하기

 테라폼은 여러 운영체제에서 사용할 수 있지만, Mac을 사용하기 때문에 brew와 tfenv를 사용해서 설치해보려고 한다.

 ftenv를 사용하면 테라폼의 버전을 쉽게 관리할 수 있다.

# tfenv 설치
brew install tfenv

# 테라폼 설치
tfenv list-remote # 설치 가능 버전 확인
tfenv install 1.5.1
tfenv use 1.5.1
tfenv list # tfenv로 설치한 버전 확인

# 테라폼 버전 정보 확인
terraform version

 

visual studio code 설치하기

 코드를 수정하기 위한 에디터로 vs code를 사용했다. 설치는 아래처럼 brew를 활용해도 좋지만 웹(https://code.visualstudio.com)에서 다운로드 받아 설치할 수도 있다.

vscode를 설치한 후엔 code 명령어를 사용할 수 있도록 Path에 추가해주면 좋다.(https://velog.io/@mingkyme/VSCode-Command-line-%EC%84%A4%EC%B9%98-code-%EB%AA%85%EB%A0%B9%EC%96%B4)

brew install visual-studio-code
 

Visual Studio Code - Code Editing. Redefined

Visual Studio Code is a code editor redefined and optimized for building and debugging modern web and cloud applications.  Visual Studio Code is free and available on your favorite platform - Linux, macOS, and Windows.

code.visualstudio.com

 

awscli 설치하기

 aws를 cli(command line interface)로 다루기 위한 툴로 brew를 사용해서 설치할 수 있다.

brew install awscli

 

유용한 도구 설치

 아래 툴들을 추가로 설치하면 작업할 때 좀 더 유용하다.

brew install tree, jq, watch

 

graphviz (dot) 플러그인 설치하기

 vscode의 마켓플레이스에서 해당 플러그인을 추가로 설치해주면, 오른쪽처럼 .dot파일을 그래프로 볼 수 있다. 

.dot 파일은 terraform graph 명령을 사용해 생성할 수 있기 때문에 위 플러그인을 사용하면 테라폼 코드의 구조를 그래프로 확인할 수 있다.

 


테라폼 사용

ec2 웹서버 배포하기

 테라폼을 사용해서 ec2에 아파치 웹 서비스를 배포해보자. 

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

resource "aws_instance" "example" {
  ami                    = "ami-0c9c942bd7bf113a2"
  instance_type          = "t2.micro"
  vpc_security_group_ids = [aws_security_group.instance.id]

  user_data = <<-EOF
              #!/bin/bash
              echo "Hello, T1012 Study 8080" > index.html
              nohup busybox httpd -f -p 8080 &
              EOF

  user_data_replace_on_change = true # true일 경우 인스턴스를 재생성한다.

  tags = {
    Name = "Single-WebSrv"
  }
}

resource "aws_security_group" "instance" {
  name = var.security_group_name

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

variable "security_group_name" {
  description = "The name of the security group"
  type        = string
  default     = "terraform-example-instance"
}

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

 

 위 코드를 apply하면, aws 서울 리전(ap-northeast-2)에 우분투(ami-0c9c942bd7bf113a2) 이미지를 사용한 t2.micro 유형의 ec2 인스턴스를 Single-WebSrv라는 이름으로 보안 그룹을 적용해서 생성한다.

 user_data에 아래 코드를 넣어 줬기 때문에 인스턴스가 실행되면서 아파치 웹 서버(8080번 포트)로 index.html을 띄운다.

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

 

 vpc와 subnet은 default를 사용하므로, 해당 리소스가 클라우드 계정 내에 없다면 생성이 실패할 수 도 있다.

이럴 경우 아래 명령을 실행해주자.

# default VPC를 생성
aws ec2 create-default-vpc

# default Subnet 생성
aws ec2 create-default-subnet --availability-zone ap-northeast-2a
aws ec2 create-default-subnet --availability-zone ap-northeast-2b
aws ec2 create-default-subnet --availability-zone ap-northeast-2c
aws ec2 create-default-subnet --availability-zone ap-northeast-2d

 

위의 모든 과정은 ~/.aws/credential에 크레덴셜 정보가 있어야 가능하다.

만약 없다면 aws configre 명령어로 설정해주면 된다.


lifecycle

 테라폼은 기본적으로 immutable한 인프라스트럭쳐 관리를 지향하기 때문에 기본 수명주기는 삭제 후 생성이지만, 작업 시 리소스를 제거하면 안되는 경우가 종종 있다.

lifecyle은 이런 경우에 리소스의 기본 수명주기를 작업자가 의도적으로 변경할 수 있도록하는 메타인수다.

메타인수를 사용하면 아래 기능들이 가능하다.

  • create_before_destroy (bool): 리소스 수정 시 신규 리소스를 우선 생성하고 기존 리소스를 삭제
  • prevent_destroy (bool): 해당 리소스를 삭제 Destroy 하려 할 때 명시적으로 거부
  • ignore_changes (list): 리소스 요소에 선언된 인수의 변경 사항을 테라폼 실행 시 무시
  • precondition: 리소스 요소에 선언해 인수의 조건을 검증
  • postcondition: Plan과 Apply 이후의 결과를 속성 값으로 검증

하지만 위의 메타인수들을 잘못 사용하면 적용은 잘 되지만 실제로는 리소스가 사라지는 경우가 있다.

 ex) 동일한 리소스를 수정할 때 create_before_destroy 옵션을 키고 적용하면 생성 후 삭제하게 되는데, 이 때 기존 리소스가 삭제됨

제대로 이해하고 사용하면 유용하지만, 잘못 사용할 경우 리스크가 있는 기능인 것 같다.

728x90
반응형
728x90
반응형

IaC (Infrastructure as Code)

 소프트웨어를 작성하는 것처럼 인프라를 코드로 관리하는 것을  IaC라고 한다.

인프라를 코드로 관리할 때의 장점은 아래와 같다.

  • 효율적인 구성 관리 : IaC로 인프라를 구성할 때의 장점 중 하나는 버전 관리 시스템(Git 등)을 활용할 수 있다는 점이다. 이를 통해 인프라의 구성을 버전별로 추적하고 이전 버전으로 되돌리는 등 인프라 구성을 쉽게 수정할 수 있다.
  • 자동화 : IaC를 사용하면 인프라가 자동으로 구성되기 때문에 사람이 구성할 때보다 신뢰성과 정확성이 증가한다.
  • 쉬운 구축과 배포: IaC를 사용하면 인프라 구축과 배포가 쉽고 빨라진다.
  • 히스토리 : 직접 구축된 서버를 보면 실제로 사용하진 않지만, 작업 중에 남겨진 흔적 등이 발견되는 경우가 있다.
    이럴 때 이력이 없다면 의미를 이해하기 쉽지않다. 하지만 코드를 통해만들어졌다면 코드를 읽어만 봐도 인프라 구성을 한 눈에 알 수 있게 된다.
  • 멱등성 : 언제 어디서 실행해도 동일한 인프라를 구성할 수 있다.

 DevOps 관점에서의 IaC는 개발자와 운영자가 SDLC 상에서 더 가까이 있을 수 있게 하고 운영을 더 명확하게 하며, 운영 업무에 소프트웨어 개발 원칙과 반복성을 적용할 수도 있다. 또한 DevOps의 핵심인 자동화와 협업을 위해서 버전 관리 시스템을 사용하여 IaC를 관리할 경우, 팀으로 하여금 효과적으로 협력하는 방법에 관한 허브 역할도 수행할 수 있다.


IaC의 종류

 코드로 인프라를 다룰 수 있게 도와주는 도구들은 여러가지가 있다.

각 도구마다 IaC의 구현 방식과 특성이 다르고, 인프라의 특성에 따라 사용할 때 이점이 다른 경우도 있다.

  • Chef (2009) : 루비 형태의 DSL(도메인 특화 언어)를 사용하여 recipe(레시피)를 작성한다. 사용을 위해 대상 서버에 별도의 agent 설치가 필요하다.  
  • Puppet (2005) : Chef와 비슷하게 루비로 작성된 DSL를 사용하고, Agent를 설치해야한다. 
  • SaltStack (2011) : ZeroMQ를 사용하여 비동기로 인프라를 구축할 수 있다. Agent가 필요하며 yaml을 사용한다. 
  • Ansible (2012) : agent-less 방식으로 ssh 접속만 가능해도 사용할 수 있다. yaml을 사용하여 코드를 작성할 수 있으며 2015년 Redhat에 인수되었다.
  • Terraform (2014) : Hashicorp에서 제공하는 오픈소스 IaC로 HCL과 JSON을 사용한다. 클라우드 인프라를 코드로 구성할 수 있다.
  • Azure Resource Manager : Microsoft Azure에서 제공하는 IaC 도구로 Azure 자원을 관리할 수 있다.
  • AWS CloudFormation : AWS에서 제공하는 IaC 도구로 AWS 자원을 관리할 수 있다.

위 내용 중 Terraform과 SaltStack, Ansible를 앞으로 블로그에 정리해볼 계획이다.

 

728x90
반응형

+ Recent posts