해당 스터디 내용은 테라폼으로 시작하는 IaC 책을 기반으로 학습하여 작성하였다.

이번주 스터디는 조건 (IF), terraform 내장함수, 프로비저너, moved, terraform_data, null_resource, multi-provider 이다. 기본 활용법을 이제 다 배웠으니, 최대한 개인적인 테스트 인프라 환경을 실제 운영하기 위한 terraform 코드를 작성한다는 목적으로 진행했다.

IF 조건


Terraform이 Cloudformation 보다 더 나은 점으로 항상 언급했던 것이 프로그래밍 적 특징을 일부 갖추고 있다는 점이었다. Terraform의 if 조건은 3항 연산자를 활용하여 리소스 배포의 유무를 결정지을 수 있다.

# variable a가 참 값이면 im true 반환, 아니면 im not 반환
# var.a == true ? "im true" : "im not"

## 예시 리소스
variable "a" {
    type = string
  default = "yes"
}
output "if-condition" {
  value = var.a == "yes" ? true : false
}
조건이 일치하기에 true가 반환되었다. 얘는 모든 리소스에 count 걸면 배포 하고 말고 정의가 될테니까 편할 듯 한데… count를 아직 잘 다룰 자신이 없다.

테라폼 내장함수


반복해서 선언해야 하거나, 편의성을 위해 활용하는 내장 함수는 주로 아래와 같다. 필요한건 예제코드나 공식 문서를 참고하는게 더 나을듯.

file(“file_path”) : terraform이 구동되는 경로를 기준으로 로컬 파일을 코드 상에서 참조할 때 쓴다. (ec2 user_data, local-exec 등)

cidrsubnet(“network_cidr”, “cidr_bit”, “number”) : 기준 네트워크 cidr 값에 추가 cidr bit를 추가하여 서브넷 네트워크 범의를 정의하는 용도로 활용한다. 주로 vpc 대역은 변수로 지정하고 나머지는 내장 함수로 처리한다.

cidrhost(“subnet_cidr”, “number”) : 얘는 네트워크 대역 내 nic의 ip를 정의하는 용도로 활용한다.

Provisoner (local-exec, remote-exec)


프로비저너는 사용자 편리성을 위해 추가된 구문으로써, tfstate로 관리되지 않는 리소스 관리 영역이기에, 멱등성을 보장할 필요가 없는 단순 작업을 terraform으로 처리하고자 할 때 활용한다.

variable "txt" {
  default   = "im txt"
}

resource "local_file" "foo" {
  content  = upper(var.txt) # 대문자로 변경해주는 terraform 내장함수
  filename = "${path.module}/foo.bar"

  provisioner "local-exec" {  # 프로비저너 중 하나인 로컬 환경에서 명령어를 실행하기 위한 리소스
    command = "echo The content is ${self.content}"  # self는 부모 리소스를 의미
  }

  provisioner "local-exec" {
    command    = "abc"
    on_failure = continue  # 실패해도 다음 명령을 실행해라
  }

  provisioner "local-exec" {
    when    = destroy # 대상 리소스 삭제시 아래 커맨드를 실행
    command = "echo The deleting filename is ${self.filename}"
  }
}
local-exec 를 활용하기 위해선 프로바이더를 설치해야 하니 init -upgrade 같은 작업을 해야한다
대문자로 변경되어 출력된 local-exec 후에 abc 라는 명령이 없어 에러가 반환됐지만 꿋꿋히 리소스 배포가 완료된 모습
에러 리소스 때문에 원래같으면 foo.bar 파일은 생성되지도 않았겠지 ㅠ
terraform destroy 진행 시 local-exec 내 when 항목으로 인해 특정 메세지가 출력된 모습

리소스 제거시 서버에서 작업자에게 노티를 하는 등 하면 좋을 것같다… 다만 tfstate에 destory 정보로 업데이트 되었을 때를 체크하는 것임을 잊어버리면 안된다. terraform은 tfstate밖에 모르는 바부다

local-exec 가 있다면 당연히 remote-exec 도 있지 않겠는가. 생각하는 방식 그대로 동작 할테지만 보통 세트 동작으로 다른 리소스와 엮어 운영한다. 이는 아래에서 서술한다.

Terraform_data (구 null_resource)


null_resource 란 별도 리소스를 규정하지 않고 이미 배포 된 리소스를 대상으로 후행 작업을 처리할 때 활용한다. 예를 들면 프로비저너의 remote-exec 같은 작업을 특정 리소스가 배포된 후에 진행하는 경우가 그렇다. 아래는 예시 코드.

resource "aws_spot_instance_request" "docker" {
  ami                         = local.arm_amazon2023
  instance_type               = "t4g.micro"
  key_name                    = "11"
  subnet_id                   = aws_subnet.pub-a.id
  associate_public_ip_address = true
  private_ip                  = cidrhost(aws_subnet.pub-a.cidr_block, 111)

  ## 별도로 운영하는 vpc 내 인스턴스를 생성 시 아래 옵션으로 진행. security_groups로 할 경우 replace로 동작한다
  vpc_security_group_ids = [aws_security_group.k8s_master.id]
  lifecycle {
    # 성가신 녀석들은 멋대로 정보 업뎃 되서 소중한 리소스를 못 없애버리게 정의해두자.
    ignore_changes = [associate_public_ip_address, user_data, ami] 
  }
  tags = {
    Name = "docker"
  }
}

위 리소스를 배포한 다음, terraform_data 를 배포한다.

variable "docker-init" {
  default = "20230721"
}

resource "terraform_data" "docker-install" {
  triggers_replace = [ # 이 곳에 정의된 리소스가 변경된 경우 terraform_data 리소스가 삭제 후 재생성됨.
    var.docker-init
  ]
  connection {
    type        = "ssh"
    user        = "ubuntu"
    private_key = file("11.pem") # 로컬 파일을 읽어오는 terraform 내장 함수.
    host        = aws_spot_instance_request.docker.public_ip
  }
  provisioner "remote-exec" {
    inline = [
      "sudo usermod -aG docker ec2-user",
      "sudo yum install -y docker",
      "sudo service docker start",
      "sudo systemctl enable docker"
    ]
  }
}

userdata로 처리해도 되는 부분이긴 한데.. 마땅한 활용 예시가 지금 떠오르지가 않는다

콘솔 상에서 원격으로 처리되는 정보를 확인할수도 있다.

보통 remote-exec 는 보안 취약점일 수 있지만… 이는 OS 규정 준수 사항 업데이트 패치ansible을 활용해서 이미 배포된 리소스의 애플리케이션을 관리하는 경우에 좀 요긴하게 쓰인다구 한다.

리소스니까 state 에서 관리되어 terraform의 멱등성을 갖게 된다.
다만 어디 원격으로 서버 내 자원 관리를 한번만 하는가. 위 코드 주석에서 본 trigger_replace 의 필요성을 깨달을 때다

실제론 어떨지 모르겠지만, 나는 변수로 날자 값을 수동 기입해서 패치 일정을 기록하는 방식으로 하면 나름 괜찮지 않나.. 싶은 생각

moved 블록


잘못된 네이밍으로 인해 참조 리소스를 새로 정의하여 배포한 아픔을 해결하는데 큰 도움을 준다. 만약 위에서 배포했던 docker 인스턴스 리소스의 이름이 맘에 안들어서 whale 로 바꾸고 싶으면 아래와 같은 순서로 진행하면 된다. (TMI. 갠적으로 테스트 환경을 terraform으로 운영해보면서 가장 감동이 컸던 리소스임)

  1. 기존 리소스의 이름을 변경한다. (state에는 여전히 변경 전 정보만 기록되있다.)
  2. moved 블럭을 생성한다
  3. terraform apply를 진행한다.
리소스 이름 바꿔주고 moved 블럭 생성
apply 시 리소스 삭제되거나 하지 않음

스터디 할때는 count도 for_each 로 바꿀 수 있다고 들었는데, 테스트 해봤는데 안됏다 ㅠ 찾아보니까 얘는 프롬프트상에서 terraform state mv {} {} 로 처리해야 한다는데, 일단 패스..

Multi Provider


권장되는 사항은 아니고 에러 발생 확률도 높기 때문에 이는 프로덕션용이 아닌 테스트 및 참고방식이다.

내가 사용할 테라폼 환경에서 AWS의 정보를 사전 정의했던 provider.tf 중 하나.

aws 프로바이더를 사용할건데, 얘가 어느 리전의 어느 IAM 인증 정보를 정의하는 부분이 있었다. 이를 alias로 복수개의 프로바이더를 지정할 수 있다. (가장 금액이 싼 버지니아 리전을 cheap로 명명)

resource "aws_s3_bucket" "tfstate" {
  provider = aws.cheap
  bucket = "ff82afdd1b"
}
성공적으로 생성 됨
그리고 이거 왜 안쓰는지도 알게됐다. alias provider 로 리소스 생성했던거 지우니까 무조건 있어야 한다고 에러가 뜬다.
tfstate를 확인하니 alias로 생성한 리소스 정보가 갱신되질 않눈다..

혹여나 이 글을 보고 있는 사람 중, 나와 동일하게 terraform alias provider로 인해 망가진 상태를 복구하기 위해서는 tfastate 내에서 alias로 생성한 리소스를 제거하여 복구할 수 있도록 하자.

만약 DynamoDB와 S3로 원격 tfstate 관리를 하고 있는 경우, DynamoDB에서 Lock 상태를 정의하는 값도 제거하자.
참고주소 : https://stackoverflow.com/questions/57984714/error-refreshing-state-state-data-in-s3-does-not-have-the-expected-content

근황 정리

3주 동안 공부한 정보를 여럿 종합해서, 현재 k8s 공부를 위해 테스트로 운영 중인 인프라 아키텍처

아래는 위 인프라를 배포하는 terraform 코드이다.

variable.tf

variable "region" {
  type    = string
  default = "ap-northeast-2"
}
variable "vpc_cidr" {
  type    = string
  default = "192.168.0.0/16"
}

provider.tf

terraform {
  required_version = "~> 1.5.2"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">=5.0, <=5.6.2"
    }
  }
  backend "s3" {
    bucket         = "9368b2f3d0-tfstate"                    # s3 bucket 이름
    key            = "terraform/study/dev/terraform.tfstate" # s3 내에서 저장되는 경로.
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

provider "aws" {
  region = var.region
}

data.tf

## 동적으로 ec2 이미지를 가장 최신 버전으로 불러오는 data block
data "aws_ami" "amzn2023_arm" {
  most_recent = true
  owners      = ["137112412989"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023*-arm64"]
  }
}

data "aws_ami" "amzn2023_amd" {
  most_recent = true
  owners      = ["137112412989"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023*-x86_64"]
  }
}

data "aws_ami" "ubuntu2204_arm" {
  most_recent = true
  owners      = ["099720109477"]

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-arm64-server*"]
  }
}
data "aws_ami" "ubuntu2204_amd" {
  most_recent = true
  owners      = ["099720109477"]

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

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

network.tf (cidrhost, cidrsubnet을 활용하여 그나마 동적으로 아이피 정의)

locals {
  azs = {
    aza = data.aws_availability_zones.azs.names[0]
    azc = data.aws_availability_zones.azs.names[2]
  }
}
## vpc networks
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr
}
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id
}
resource "aws_subnet" "pub-a" {
  vpc_id                                      = aws_vpc.main.id
   # vpc cidr(x.x.0.0/16)에 8비트 추가, 및 비트 추가 = x.x.1.0/24
  cidr_block                                  = cidrsubnet(var.vpc_cidr, 8, 1)
  enable_resource_name_dns_a_record_on_launch = true
  availability_zone                           = local.azs.aza
}
resource "aws_subnet" "priv-a" {
  vpc_id                                      = aws_vpc.main.id
   # vpc cidr(x.x.0.0/16)에 8비트 추가, 및 비트 추가 = x.x.128.0/24
  cidr_block                                  = cidrsubnet(var.vpc_cidr, 8, 128)
  enable_resource_name_dns_a_record_on_launch = true
  availability_zone                           = local.azs.aza
}

# 로컬에 대한 라우팅은 기본적으로 생성되있음. 추가할거만 넣으면 됨
resource "aws_route_table" "pub_route" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
}
resource "aws_route_table" "priv_route" {
  vpc_id = aws_vpc.main.id
}

# 라우트 테이블에 서브넷 정보 등록
resource "aws_route_table_association" "pub_rt_associate" {
  subnet_id      = aws_subnet.pub-a.id
  route_table_id = aws_route_table.pub_route.id
}

resource "aws_route_table_association" "priv_rt_associate" {
  subnet_id      = aws_subnet.priv-a.id
  route_table_id = aws_route_table.priv_route.id
}

instance.tf (보안그룹에 반복문을 적용하여 가장 많이 바꼇다)

locals {
  arm_ubuntu2204 = data.aws_ami.ubuntu2204_arm.image_id
  arm_amazon2023 = data.aws_ami.amzn2023_arm.image_id
  x86_ubuntu2204 = data.aws_ami.ubuntu2204_amd.image_id
  x86_amazon2023 = data.aws_ami.amzn2023_amd.image_id
  default_tags = {
    manage = "Terraform"
  }
  ingress_rules = {
    vpc = {
      description = "vpc_allow_all"
      from_port   = null
      to_port     = null
      protocol    = "-1"
      cidr_ipv4   = var.vpc_cidr
    }
    home_ssh = {
      description = "ssh allow my home"
      from_port   = null
      to_port     = null
      protocol    = "-1"
      cidr_ipv4   = "14.0.0.0/8"
    }
  }
}

# security group
resource "aws_security_group" "k8s_master" {
  name        = "k8s_master"
  description = "k8s_master"
  vpc_id      = aws_vpc.main.id
  tags        = local.default_tags
}

# 다른 SG 참조시에 대한 요소를 효과적으로 처리하기 위해. 
resource "aws_vpc_security_group_ingress_rule" "k8s_master" {
  for_each          = local.ingress_rules
  security_group_id = aws_security_group.k8s_master.id
  from_port         = each.value.from_port
  to_port           = each.value.to_port
  ip_protocol       = each.value.protocol
  cidr_ipv4         = each.value.cidr_ipv4
  description       = each.value.description
}
resource "aws_vpc_security_group_egress_rule" "k8s_master" {
  security_group_id = aws_security_group.k8s_master.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
  #   from_port         = -1
  #   to_port           = -1
}

resource "aws_instance" "k8s_master" {
  ami                         = local.arm_ubuntu2204
  instance_type               = "t4g.small"
  key_name                    = "11"
  subnet_id                   = aws_subnet.pub-a.id
  associate_public_ip_address = true
  private_ip                  = cidrhost(aws_subnet.pub-a.cidr_block, 15)

  ## 별도로 운영하는 vpc 내 인스턴스를 생성 시 아래 옵션으로 진행. security_groups로 할 경우 replace로 동작한다
  vpc_security_group_ids = [aws_security_group.k8s_master.id]
  user_data = file("bash_script/k8s-master.sh")
  lifecycle {
    ignore_changes = [associate_public_ip_address, ami, user_data]
  }
  tags = {
    Name = "k8s_master"
  }
}

resource "aws_spot_instance_request" "k8s_worker1" {
  ami                         = local.arm_ubuntu2204  
  instance_type               = "t4g.small"
  key_name                    = "11"
  subnet_id                   = aws_subnet.pub-a.id
  associate_public_ip_address = true
  private_ip                  = cidrhost(aws_subnet.pub-a.cidr_block, 16)

  ## 별도로 운영하는 vpc 내 인스턴스를 생성 시 아래 옵션으로 진행. security_groups로 할 경우 replace로 동작한다
  vpc_security_group_ids = [aws_security_group.k8s_master.id]
  user_data = file("bash_script/k8s-worker.sh")
  lifecycle {
    ignore_changes = [associate_public_ip_address, user_data, ami] # spot은 userdata 변경되면 적용할라고 삭제후 생성되기때문
  }
  tags = {
    Name = "k8s_worker1"
  }
}

output "k8s" {
  value = [
    "pub : ${aws_instance.k8s_master.public_ip}", 
    "priv : ${aws_instance.k8s_master.private_ip}"
  ]
}
output "k8s_worker1" {
  value = [
    "pub : ${aws_spot_instance_request.k8s_worker1.public_ip}", 
    "priv : ${aws_spot_instance_request.k8s_worker1.private_ip}"
  ]
}

user_data에 file로 선언한 스크립트는 이 블로그 게시글인 복붙으로 구축하는 Kubernetes 구축 스크립트. With Containerd,kubeadm 에서 확인 가능하다.


ash

AWS를 활용하여 고객사 MSP 업무를 수행하고 있습니다.

0개의 댓글

답글 남기기

Avatar placeholder

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다