[AWS EKS Workshop Study] 8주차 - IaC

JoonHyeok Han·2024년 4월 27일
0

개요

EKS 스터디 마지막 주차는 인프라를 코드로 관리하는 IaC(Infrastructure as Code)를 학습했다.

그 중에서도 Terraform 의 기본적인 사용법부터 EKS 클러스터를 설치하는 방법까지 전반적으로 살펴보았다.

EKS 나 쿠버네티스에 대한 이해 없이 테라폼을 바로 사용하기에는 학습 곡선이 꽤나 가파르기 때문에 먼저 쿠버네티스를 이해하고 사용할 것을 권장한다.

Terraform

출처: Terraform

Terraform(이하 테라폼)은 클라우드 서비스나 온프레미스 환경에서 인프라를 코드 로 관리할 수 있도록 지원하는 오픈소스이다.

위의 이미지에서 Terraform provider 는 AWS, Azure, GCP, Oracle Cloud 등 다양한 업체를 의미하고, Target API 는 Terraform provider 에서 운영하고 있는 클라우드 서비스의 API 를 의미한다.

예를 들어, 테라폼을 이용하면 AWS EC2 나 EKS 환경을 코드로 설치하는 것이 가능하다. 서울 리전에 EC2 인스턴스를 설치하는 예시는 아래와 같다.

provider "aws" {
  region = "ap-northeast-2"        // 원하는 리전을 선택합니다.
}

resource "aws_instance" "example" {
  ami           = "ami-123456789"  // 사용할 AMI의 ID를 입력합니다.
  instance_type = "t2.micro"       // 인스턴스 유형을 선택합니다.

  key_name      = "my-key-pair"    // 키페어의 이름을 입력합니다.

  security_groups = ["my-security-group"]  // 사용할 보안 그룹의 이름을 입력합니다.

  tags = {
    Name = "ExampleInstance"
  }
}

핵심 작동 원리

테라폼의 핵심 작동 원리는 아래의 이미지와 같이 3단계로 나뉜다.

출처: Terraform

  1. Write: 위에서 AWS EC2 인스턴스를 실행하는 예시 코드와 같이 구성하고자 하는 인프라를 코드로 작성하는 단계이다. 파일 확장자는 .tf 를 사용하며, HCL(HashiCorp Configuration Language)라는 언어로 작성한다.
  2. Plan: Write 단계에서 작성한 파일을 분석해서 실행 전에 어떤 변경 사항이 발생할 지 확인하는 과정이다. 현재 인프라 상태와 적용하고자 하는 상태의 차이를 분석하고 필요한 변경 사항을 결정한다.
  3. Apply: Plan 단계에서 생성된 실행 계획을 실제로 적용해서 인프라를 변경한다. 클라우드 제공업체의 API 호출을 통해 인프라 리소스를 생성, 업데이트 또는 삭제한다.

Terraform 실습 준비

Terraform 은 로컬 환경에서 클라우드 서비스를 이용할 수 있다.

MacOS 에서 설치했으며, 패키지 관리는 homebrew 를 이용했다.

Terraform 설치

공식 문서에서 설명하는 대로 설치를 진행한다. (링크)

brew tap hashicorp/tap
brew install hashicorp/tap/terraform

설치가 잘 되었는지 확인하기 위해 아래의 명령어를 실행한다.

terraform --help
#Usage: terraform [global options] <subcommand> [args]
#
#The available commands for execution are listed below.
#The primary workflow commands are given first, followed by
#less common or more advanced commands.
#
#Main commands:
#  init          Prepare your working directory for other commands
#  validate      Check whether the configuration is valid

tfenv 설치

테라폼은 여러 버전을 바꿔가면서 사용하는 것이 가능하다.

이를 쉽게 도와주는 것이 tfenv 이다.

필수로 설치할 필요는 없는 것 같지만, 다른 버전을 바꿔서 사용한다면 반드시 설치하자.

이미 brew 로 테라폼을 설치했다면 아래의 명령어를 먼저 실행하자.

brew unlink terraform

그 다음 tfenv 를 설치한다.

brew install tfenv

설치가 끝나면 아래의 명령어를 이용해서 설치 가능한 버전 목록을 확인할 수 있다.

tfenv list-remote

특정 버전의 버전을 설치하려면 아래의 명령어를 실행한다.

여기서는 1.8.1 버전을 설치했다.

tfenv install 1.8.1

특정 버전을 사용하려면 아래의 명령어를 실행한다.

tfenv use 1.8.1

자동완성 플러그인 설치

tab 을 입력하면 사용 가능한 명령어를 보여주는 플러그인을 설치하려면 아래의 명령어를 실행한다.

terraform -install-autocomplete

위의 명령어를 실행하면 ~/.zshrc 파일에 아래와 같은 내용이 추가된다.

autoload -U +X bashcompinit && bashcompinit
complete -o nospace -C /usr/local/bin/terraform terraform

적용을 위해 아래의 명령어를 실행한다.

source ~/.zshrc

터미널에서 terraform 을 입력하고 tab 을 누르면 아래의 이미지와 같이 사용 가능한 명령어의 목록이 나오는 것을 확인할 수 있다.

AWS CLI 설치

AWS 에 인프라를 구성하기 위해 AWS CLI 를 설치했다.

brew install awscli

설치 이후 aws configure 명령어를 실행해서 자격 증명을 해주어야 한다.

자격 증명에 대한 설명은 생략한다.

eksctl 설치

AWS EKS 를 이용하기 위해 eksctl 을 설치했다.

brew install eksctl

kubectl 설치

eksctl 과 마찬가지로 쿠버네티스 API 서버를 이용하기 위해 kubectl 을 설치했다.

brew install kubernetes-cli 

helm 설치

EKS 에 패키지를 간편하게 설치하기 위한 helm 를 설치했다.

brew install helm

편리한 툴

실습을 편리하게 진행하기 위한 툴도 함께 설치했다.

brew install tree jq watch

Visual Studio Code Extension

테라폼 파일을 작성할 때 HCL 문법에 맞게 하이라이팅을 해주는 Extension 을 설치했다.

Terraform 실습

AWS EC2 인스턴스 생성하기

테라폼 파일 생성

실습을 위한 폴더를 하나 만들어서 main.tf 파일을 생성했다.

mkdir -p ~/workspace/terraform-demo/1
cd ~/workspace/terraform-demo/1
touch main.tf

EC2 의 운영체제는 Amazon Linux 2 를 사용할 것이다.

이를 위해 최신 AMI ID 를 찾아서 환경변수로 저장했다.

AL2ID=`aws ec2 describe-images --owners amazon --filters "Name=name,Values=amzn2-ami-hvm-2.0.*-x86_64-gp2" "Name=state,Values=available" --query 'Images|sort_by(@, &CreationDate)[-1].[ImageId]' --output text`
echo $AL2ID
#ami-0217b147346e48e84

EC2 를 생성하기 위한 코드를 아래와 같이 작성해서 저장했다.

cat <<EOT > main.tf
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_instance" "example" {
  ami           = "$AL2ID"
  instance_type = "t2.micro"
}
EOT

위에서 사용한 HCL 문법을 간단하게 정리하면 아래와 같다.

  • provider: 테라폼으로 정의할 Infrastructure Provider(AWS, GCP 등)을 의미한다.
  • resource “_” “” { [config …] }
    • provider: aws 와 같은 인프라 공급자 이름
    • type: security_group 과 같은 리소스의 유형. 여기서는 EC2 를 생성하기 때문에 instance 로 작성
    • name: 리소스의 이름
    • config: 한 개 이상의 arguments. 여기서는 AMI 와 instance type 을 정의.

terraform init

main.tf 에 작성한 파일을 기반으로 테라폼을 초기화 하기 위해 아래의 명령어를 실행한다.

terraform init

이 명령어를 실행하면 .terraform 폴더와 .terraform.lock.hcl 파일이 생성된다.

  • .terraform.lock.hcl : 테라폼이 사용하는 모듈의 의존성 버전을 관리하기 위한 파일이다. Node.js 프로젝트에서 사용하는 package.lock.json 과 동일한 역할을 한다.
  • .terraform : 테라폼에서 사용하는 모든 플러그인 및 모듈을 저장하는 폴더다. Node.js 프로젝트에서 사용하는 node_modules 폴더와 동일한 역할을 한다.

명령어 실행 결과로 .terraform 폴더에는 아래와 같은 폴더 구조와 terraform-provider-aws_v5.46.0_x5 라는 파일이 생성되었다.

.terraform
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── aws
                └── 5.46.0
                    └── darwin_amd64
                        └── terraform-provider-aws_v5.46.0_x5

terraform-provider-aws_v5.46.0_x5 는 테라폼으로 작성한 코드를 AWS API 로 변환해서 AWS 리소스를 이용할 수 있도록 변환하는 플러그인이라 생각하면 된다.

terraform plan

테라폼이 실행할 변경 사항을 미리 확인하고, 실행 전에 어떻게 반영할 지 확인하기 위해 terraform plan 명령어를 실행한다.

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" {
      + ami                                  = "ami-0217b147346e48e84"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = (known after apply)
      + availability_zone                    = (known after apply)
      + cpu_core_count                       = (known after apply)
      + cpu_threads_per_core                 = (known after apply)
      + disable_api_stop                     = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + host_resource_group_arn              = (known after apply)
      + iam_instance_profile                 = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_lifecycle                   = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t2.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = (known after apply)
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (known after apply)
      + primary_network_interface_id         = (known after apply)
      + private_dns                          = (known after apply)
      + private_ip                           = (known after apply)
      + public_dns                           = (known after apply)
      + public_ip                            = (known after apply)
      + secondary_private_ips                = (known after apply)
      + security_groups                      = (known after apply)
      + source_dest_check                    = true
      + spot_instance_request_id             = (known after apply)
      + subnet_id                            = (known after apply)
      + tags_all                             = (known after apply)
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = false
      + vpc_security_group_ids               = (known after apply)
    }

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

AMI 와 instance type 이 지정된 리소스로 create 하겠다는 출력이 나왔다.

terraform apply

plan 의 결과를 확인하고 적용하기 위해 terraform apply 명령어를 실행한다.

terraform apply

변경 사항을 적용하겠냐는 확인을 하는데, yes 를 입력한다.

...
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 # yes 입력

만약, 기본 VPC 나 기본 서브넷이 없다면 설치가 안되기 때문에 생성 후에 다시 시도하자.

# 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

기본 VPC 와 서브넷은 요금이 청구되지 않으니 안심해도 된다.

생성이 끝나면 아래의 명령어로 세부 사항을 확인할 수 있다.

terraform show

AWS 콘솔 페이지에서도 EC2 가 생성된 것을 확인할 수 있다.

terraform destroy

리소스를 삭제하기 위해 terraform destroy 명령어를 사용할 수 있다.

terraform destroy

apply 명령어와 마찬가지로 yes 를 입력해야 삭제가 된다.

자동으로 삭제하고 싶다면 -auto-approve 옵션을 추가하면 된다.

terraform destroy -auto-approve

HCL

위에서 언급했듯 HCL 은 코드를 통해 인프라를 관리할 수 있는 문법이다.

일반적인 프로그래밍 언어처럼 조건문을 사용하거나 변수와 문자열을 함께 사용하는 String Interpolation 을 지원한다.

  • String Interpolation 이란 JavaScript 에서 사용하는 문법을 생각하면 된다.
    const name = "Jay";
    console.log(`Hello World, ${Jay}`); // String Interpolation 
  • ${Jay} 는 변수인데, 위와 같이 작성하면 변수를 문자열로 변환해서 사용할 수 있게 된다.

동일한 내용을 JSON 으로 표현하는 것보다 간결하고 읽기 쉽게 작성할 수 있다.

비교를 위해 HCL 을 이용한 테라폼 구성과 JSON 을 이용한 CloudFormation 구성을 비교해보자.

HCL vs JSON

HCL

resource "local_file" "abc" {
  content = "abc!"
  filename = "${path.module}/abc.txt"
  metadata = {
	  name = "{$var.PilotServerName}-vm"
	}
}

JSON

{
  "resource": [
  {
    "local_file": [
      {
        "abc": [
          {
            "content":"abc!",
            "filename":"${path.module}/abc.txt"
            "metadata": {
		           "name":"{"Fn::Join":["-",[PilotServerName,vm]]}"            
            }
          }
        ]
     }
   ]
  }
 ]
}

JSON 보다 HCL 이 조금 더 간결하고 읽기 쉬운 것을 알 수 있다.

표현식

HCL 표현식에는 일반적인 프로그래밍 언어처럼 주석, 변수 정의, 함수를 제공하고 있다.

// 한줄 주석 방법1
# 한줄 주석 방법2

/*
라인
주석
*/

locals {
  key1     = "value1"     # = 를 기준으로 키와 값이 구분되며
  myStr    = "TF ♡ UTF-8" # UTF-8 문자를 지원한다.
  multiStr = <<EOF
  Multi
  Line
  String
  with anytext
EOF

  boolean1    = true   # boolean true
  boolean2    = false  # boolean false를 지원한다.
  deciaml     = 123    # 기본적으로 숫자는 10진수,
  octal       = 0123   # 0으로 시작하는 숫자는 8진수,
  hexadecimal = "0xD5" # 0x 값을 포함하는 스트링은 16진수,
  scientific  = 1e10   # 과학표기 법도 지원한다.

  # funtion 호출 예
  myprojectname = format("%s is myproject name", var.project)

  # 3항 연산자 조건문을 지원한다.
  credentials = var.credentials == "" ? file(var.credentials_file) : var.credentials
}

block

테라폼에서는 resource, data, output, variable 와 같은 블럭들로 코드를 구성할 수 있다.

파일마다 한 가지 블록만 작성하도록 하는 것이 유지보수 측면에서 권장된다.

규모가 작은 프로젝트라면 하나의 파일에 합쳐서 작성할 수도 있지만, 확장성과 가독성을 위해서는 분리해서 작성하는 것이 낫다.

예를 들면, 아래와 같이 파일을 분리해서 작성할 수 있다.

.
├── main.tf
├── variables.tf
├── outputs.tf
├── <resource_name>.tf
└── data.tf

main.tfterraform apply 명령어를 실행하면 가장 먼저 실행하는 파일이다.

terraform

테라폼 버전이나 프로바이더 버전은 자동으로 설정되지만, 다른 사람들과 함께 작업할 때는 버전을 명시적으로 선언하고, 필요한 조건을 입력해서 실행 오류를 최소화 하는 거싱 권장된다.

이는 오늘 실행한 결과와 3년 후에 실행한 결과가 동일하게 하기 위함이다.

terraform 블록에서 버전을 명시하는 예시는 아래와 같다.

terraform {
  required_version = "~> 1.3.0" # 테라폼 버전

  required_providers { # 프로바이더 버전을 나열
    random = {
      version = ">= 3.0.0, < 3.1.0"
    }
    aws = {
      version = "4.2.0"
    }
  }

  cloud { # Cloud/Enterprise 같은 원격 실행을 위한 정보
    organization = "<MY_ORG_NAME>"
    workspaces {
      name = "my-first-workspace"
    }
  }

  backend "local" { # state를 보관하는 위치를 지정
    path = "relative/path/to/terraform.tfstate"
  }
}

버전 체계는 Sematic Versioning 방식을 따른다.

  • Major: 내부 동작 API 변경 또는 삭제. 하위 호환이 되지 않는 버전
  • Minor: 신규 기능 추가, 개선. 하위 호환이 가능한 버전
  • Patch: 버그 및 일부 기능이 개선된 하위 호환이 가능한 버전

버전 제약 구문은 아래와 같은 규칙을 따른다.

  • = 또는 연산자 없음: 지정된 버전만을 허용. 다른 버전과 함께 사용 불가.
  • != : 지정한 버전을 제외.
  • >, >=, <, <= : 지정한 버전과 비교해 조건에 맞는 경우만 허용.
  • ~> : 지정한 버전에서 가장 자리수가 낮은 구성요소만 증가하는 것을 허용.
    • ~> 1.4 의 경우, 1.4 버전보다 큰 1.5, 1.6 버전을 허용
    • ~> 1.4.5 의 경우, 1.4.5 버전보다 큰 1.4.6, 1.4.7 버전을 허용

버전 명시에 따른 테라폼 실행 결과를 확인하기 위해 다른 디렉토리를 생성해서 확인해보자.

mkdir -p ~/workspace/terraform-demo/2
cd ~/workspace/terraform-demo/2

terraform version
#Terraform v1.8.2
#on darwin_amd64

cat << EOT > main.tf
terraform {
  required_version = "< 1.0.0"
}

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

terraform init

로컬에 설치한 테라폼 버전은 1.8.2 버전이지만, 1.0.0 버전보다 낮은 버전의 테라폼을 사용하도록 명시했다.

그렇기에 terraform init 명령어를 실행하면 아래와 같은 에러 메세지가 출력된다.

Initializing the backend...
╷
│ Error: Unsupported Terraform Core version
│ 
│   on main.tf line 2, in terraform:
│    2:   required_version = "< 1.0.0"
│ 
│ This configuration does not support Terraform version 1.8.2. To proceed,
│ either choose another supported Terraform version or update this version
│ constraint. Version constraints are normally set for good reason, so
│ updating the constraint may lead to other errors or unexpected behavior.

프로바이더 버전

테라폼 0.13 버전 이전에서는 provider 블록에 함께 버전을 명시했지만, 0.13 이후 버전에서는 프로바이더 버전은 terraform 블록에서 required_providers 에 정의하도록 변경되었다.

[v0.13 이전]

provider "aws" {
  version = "~> 4.2.0"
  region = "ap-northeast-2"
}

provider "azurerm" {
  version = ">= 2.99.0"
  features {}
}

[v0.13 이후]

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 4.2.0"
    }
    azurerm = {
      source = "hashicorp/azurerm"
      version = ">= 2.99.0"
    }
  }
}

테라폼에서 지원하는 프로바이더는 아래의 링크에서 확인 가능하다.

AWS 를 사용하고자 한다면, USE PROVIDER 버튼을 클릭하면 아래의 이미지와 같이 required_providers 블록이 표시된다.

resource

프로바이더가 제공하는 리소스의 타입이다.

예를 들면, AWS 는 aws_instnace 라는 타입을 제공하는데, 이를 이용해서 EC2 인스턴스를 생성하고 조작하는 것이 가능하다.

resource "aws_instance" "example" {
  ami           = "ami-0217b147346e48e84"
  instance_type = "t2.micro"
}

리소스는 아래의 동작들을 정의할 수 있다.

  • depends_on: 종속성 선언. 선언된 구성요소와의 생성 시점을 정의
  • count: 선언된 개수에 따라 여러 리소스 생성
  • for_each: map 또는 set 타입의 데이터 배열의 값을 기준으로 여러 리소스 생성
  • provider: 동일한 프로바이더가 다수 정의된 경우 지정
  • lifecycle: 리소스의 수명주기 관리
  • provisioner: 리소스 생성 후 추가 작업 정의
  • timeouts: 프로바이더에서 정의한 일부 리소스 유형에서는 create, update, delete 에 대한 허용 시간 정의 가능

테라폼 종속성은 자동으로 연관 관계가 정의되지만, 명시적으로 종속성을 부여하고자 할 때는 depends_on 을 이용한다.

resource "local_file" "abc" {
  content  = "123!"
  filename = "${path.module}/abc.txt"
}

resource "local_file" "def" {
  content  = **local_file.abc.content**
  filename = "${path.module}/def.txt"
}

위와 같이 작성하면 def.txt 파일은 abc.txt 의 내용인 123! 을 가져온다. 이때, 리소스 defabc 가 먼저 만들어져야 하기 때문에 abc 에 종속성이 생긴다.

terraform apply 명령어를 실행하면 실제로 def.txtabc.txt 파일이 먼저 생성되고나서 생성되는 것을 확인할 수 있다.

Plan: 2 to add, 0 to change, 0 to destroy.
local_file.abc: Creating... # abc.txt 생성
local_file.abc: Creation complete after 0s [id=5f30576af23a25b7f44fa7f5fdf70325ee389155]
local_file.def: Creating... # def.txt 생성
local_file.def: Creation complete after 0s [id=5f30576af23a25b7f44fa7f5fdf70325ee389155]

아래와 같이 depends_on 를 이용해서 종속성을 적용하는 것도 가능하다.

resource "local_file" "abc" {
  content  = "123!"
  filename = "${path.module}/abc.txt"
}

resource "local_file" "def" {
  depends_on = [
    local_file.abc
  ]

  content  = "456!"
  filename = "${path.module}/def.txt"
}

리소스 구성에서 참조 가능한 값은 인수와 속성이다.

  • 인수: 리소스 생성 시 사용자가 선언하는 값
  • 속성: 사용자가 설정하는 것은 불가능하지만, 리소스 생성 이후 획득 가능한 리소스 고유 값

리소스 인수 선언과 참조 가능한 인수 및 속성의 패턴은 아래와 같다.

# Terraform Code
resource "<리소스 유형>" "<이름>" {
  <인수> = <>
}

# 리소스 참조
<리소스 유형>.<이름>.<인수>
<리소스 유형>.<이름>.<속성>

쿠버네티스 네임스페이스 리소스 인수 값을 참조해서 secret 을 생성하는 예시 코드는 아래와 같다.

resource "kubernetes_namespace" "example" {
  metadata {
    annotations = {
      name = "example-annotation"
    }
    name = "terraform-example-namespace" # 이 값을 참조
  }
}

resource "kubernetes_secret" "example" {
  metadata {
    namespace = kubernetes_namespace.example.metadata.0.name 
    name      = "terraform-example"
  }
  data = {
    password = "P4ssw0rd"
  }
}

variable

variable 블록을 이용하면 변수를 정의할 수 있다.

타입과 기본 값을 지정할 수 있으며, 타입에는 string, number, bool, list, map, set, tuple, object 가 있다.

변수 타입에 대한 자세한 내용은 아래의 링크에서 참고할 수 있다.

# 예시 1
variable "env" {
	type = string
  default = "dev"
}

# 예시 2
variable "env" {}

예시 2에서 괄호 안을 비운 채로 정의하면 terraform plan , terraform apply 와 같은 명령어를 실행할 때 변수의 값을 입력받는다.

terraform plan

var.env
  Enter a value:

외부에 노출되면 안되는 민감한 정보를 관리할 때 이 방법을 쓰지만, 매번 입력해주는 번거로움 때문에 .tfvars 를 이용하는 것을 권장한다.

변수 정의할 때 사용할 수 있는 메타인수는 아래와 같다.

  • default : 변수 값을 전달하는 여러 가지 방법을 지정하지 않으면 기본값이 전달된다. 기본값이 없으면 대화식으로 사용자에게 변수 값을 입력 받음
  • type : 변수에 허용되는 값 유형 정의한다. 유형을 지정하지 않으면 any 유형으로 간주한다.
  • description : 입력 변수의 설명
  • validation : 변수 선언의 제약조건을 추가해 유효성 검사 규칙을 정의 - 링크
  • sensitive : 민감한 변수 값임을 알리고 테라폼의 출력문에서 값 노출을 제한 (암호 등 민감 데이터의 경우) - 링크
  • nullable : 변수에 값이 없어도 됨을 지정

다른 자료형들은 아래와 같이 사용할 수 있다.

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
}

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" }]
}

validation 으로 유효성을 검사하는 예시는 아래와 같다.

variable "image_id" {
  type        = string
  description = "The id of the machine image (AMI) to use for the server."

  validation {
    condition     = length(var.image_id) > 4
    error_message = "The image_id value must exceed 4."
  }

  validation {
    # regex(...) fails if it cannot find a match
    condition     = can(regex("^ami-", var.image_id))
    error_message = "The image_id value must starting with \"ami-\"."
  }
}

regex 함수는 문자열에 정규식을 적용해서 일치하는 문자열을 반환하는데, can 함수를 함께 사용하면 정규식이 일치하지 않는 경우의 오류를 검출할 수 있다.

validation 블록은 여러 개 선언할 수 있다.

변수는 var.<이름> 형태로 참조할 수 있다.

variable "my_password" {}

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

민감한 변수는 sensitive 인수를 추가해서 선언할 수 있다.

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

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

terraform console 로 확인하면 값이 표시되지 않는 것을 확인할 수 있다.

echo "local_file.abc.content" | terraform console
(sensitive value)

하지만 terraform.tfstate 파일에는 암호화 되지 않은 평문으로 저장되기 때문에 state 파일의 보안을 신경써야 한다.

cat terraform.tfstate | grep '"content":'
            "content": "password",

.tfvars

.tfvars 파일에는 .env 파일과 유사하게 민감한 값을 저장할 때 사용한다.

.gitignore 와 같은 파일에 버전 관리 툴에 추적되지 않도록 .tfvars 추가해주어야 한다.

아래와 같이 resource.tfvars 파일을 생성한다.

#resource.tfvars

name = "jay"

그 다음 resource 를 아래와 같이 정의한다.

.tfvars 파일을 사용하려면 variable 블록을 함께 선언해주어야 한다.

variable "name" {
  type = string
}

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

terraform apply 명령어를 사용할 때는 -var-file 옵션을 추가해야 한다.

terraform apply -var-file="resource.tfvars" -auto-approve

실행 결과로 만들어진 abc.txt 에는 resource.tfvars 에 정의한 jay 라는 문자열이 저장된 것을 확인할 수 있다.

cat abc.txt
#jay

output

어떤 값을 내보낼 지 정의하는 블록이다.

EC2 리소스 id 를 출력할 때, 아래처럼 정의할 수 있다.

output "ec2_id" {
    value = aws_instance.example.id
}

terraform apply 명령어를 실행하면 아래와 같이 값이 출력된다.

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

Outputs:

ec2_id = i-0631d838df0ae08d1

output 은 터미널에서 출력 값을 확인하는 목적도 있지만, 다른 디렉토리에서 특정 값을 참조할 수 있도록 하는 목적이 더 크다.

data

프로바이더에서 제공하는 리소스 정보를 가져와 테라폼에서 사용할 수 있는 상태로 맵핑한다.

data "aws_iam_policy" "policy" {
    arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}

위와 같이 작성하면 AWS 의 IAM Policy 중에서 AdministratorAccess 의 ARN 을 가져오는 것이다.

ARN 이 아니더라도 filter 를 이용해서 원하는 리소스를 검색할 수 있다.

data "aws_ami" "web" {
  filter {
    name   = "state"
    values = ["available"]
  }
  most_recent = true
}

데이터 소스를 이용해서 resource 를 정의하는 것도 가능하다.

# Declare the data source
data "aws_availability_zones" "available" {
  state = "available"
}
resource "aws_subnet" "primary" {
  availability_zone = data.aws_availability_zones.available.names[0]
  # e.g. ap-northeast-2a
}
resource "aws_subnet" "secondary" {
  availability_zone = data.aws_availability_zones.available.names[1]
  # e.g. ap-northeast-2b
}

terraform console 을 이용하면 데이터 소스 참조를 확인할 수도 있다.

우선 아래와 같이 data 리소스를 정의한다.

data "local_file" "abc" {
  filename = "${path.module}/abc.txt"
}

그 다음 abc.txt 파일을 생성하고, init - plan - apply 를 실행한 후 terraform console 을 실행하면 데이터 소스를 확인할 수 있다.

data.<리소스 유형>.<이름>.<속성> 의 형태로 확인 가능하다.

echo "hello world" > abc.txt

terraform init && \
terraform plan && \
terraform apply -auto-approve

terraform console
> data.local_file.abc.filename
"./abc.txt"
> data.local_file.abc.content
<<EOT
hello world

EOT
> data.local_file.abc.id
"22596363b3de40b06f981fb85d82312e8c0ed511"
exit

모듈화

테라폼은 루트 모듈과 자식 모듈로 구분해서 인프라를 관리할 수 있다.

작은 규모라면 하나의 파일 안에서 관리할 수도 있겠지만, 규모가 커진다면 잘게 나누고 쪼개야 유지보수가 용이해진다.

모듈화 구조

테라폼 모듈은 일반적으로 아래와 같은 파일들로 구성한다.

파일명목적
main.tfresource, data 블록 정의
variables.tf모듈에서 사용할 variable 블록 정의
outputs.tf모듈에서 생성한 resource 의 출력값을 외부 모듈에서 사용하기 위해 내보냄. JavaScript 의 export 키워드와 동일한 역할을 함

디렉토리를 트리 구조로 살펴보면 아래와 같다.

module-demo
├── 0-vpc
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── 1-sg
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── 2-ec2
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── main.tf

루트 모듈

최상단의 main.tf 파일은 하위 모듈을 정의하고, 각 모듈에서 사용할 변수나 종속성을 정의한다.

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

# VPC 모듈 호출
module "vpc" {
  source = "./0-vpc"
  vpc_cidr_block    = "10.10.0.0/16"
  subnet1_cidr_block = "10.10.1.0/24"
  subnet2_cidr_block = "10.10.2.0/24"
}

# 보안 그룹 모듈 호출
module "security_group" {
  source = "./1-sg"
  vpc_id = module.vpc.vpc_id
}

# EC2 인스턴스 모듈 호출
module "ec2_instance" {
  source           = "./2-ec2"
  instance_type    = "t2.micro"
  security_group_id = module.security_group.security_group_id
  mysubnet1_id = module.vpc.mysubnet1_id
  depends_on = [
    module.security_group,
    module.vpc
  ]
}

서브 모듈

서브 모듈의 main.tf 는 아래와 같이 resource 블록을 정의한다.

# 1-sg/main.tf

resource "aws_security_group" "mysg" {
  vpc_id      = var.vpc_id
  name        = "terraform SG"
  description = "terraform demo SG"
}

resource "aws_security_group_rule" "mysginbound" {
  type              = "ingress"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.mysg.id
}

resource "aws_security_group_rule" "mysgoutbound" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.mysg.id
}

outputs.tf 는 다른 모듈에서 사용할 값을 정의한다.

# 1-sg/outputs.tf
output "security_group_id" {
  value = aws_security_group.mysg.id
}

variables.tf 는 루트 모듈에서 주입하는 변수를 정의하기 위해 사용할 수 있다.

variable "vpc_id" {
  description = "The VPC ID where the security group will be created."
  type        = string
}

실습

아래와 같은 구조로 EC2 인스턴스에 웹 서버를 실행해보자.

디렉토리 구조는 아래와 같다.

module-demo
├── 0-vpc
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── 1-sg
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── 2-ec2
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── main.tf

module-demo/main.tf

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

# VPC 모듈 호출
module "vpc" {
  source = "./0-vpc"
  vpc_cidr_block    = "10.10.0.0/16"
  subnet_cidr_block = "10.10.1.0/24"
}

# 보안 그룹 모듈 호출
module "security_group" {
  source = "./1-sg"
  vpc_id = module.vpc.vpc_id
}

# EC2 인스턴스 모듈 호출
module "ec2_instance" {
  source           = "./2-ec2"
  instance_type    = "t2.micro"
  security_group_id = module.security_group.security_group_id
  mysubnet_id = module.vpc.mysubnet_id
  depends_on = [
    module.security_group,
    module.vpc
  ]
}

EC2 인스턴스 모듈의 depends_on 은 명시적으로 종속성을 정의하기 위해 사용한다.

명시하지 않아도 내부적으로 자동으로 실행 순서를 정한다고 하는데, 정확한 순서에 따라 생성하고 싶다면 사용하는 것이 좋은 것 같다.

모듈에서 다른 모듈의 변수를 사용하고 싶다면 module.모듈명.출력값 형태로 사용할 수 있다.

출력값은 모듈의 [outputs.tf](http://outputs.tf) 파일에 정의한 출력값을 의미한다.

module-demo/0-vpc/main.tf

resource "aws_vpc" "myvpc" {
  cidr_block = var.vpc_cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "terraform-demo-vpc"
  }
}

resource "aws_subnet" "mysubnet" {
  vpc_id     = aws_vpc.myvpc.id
  cidr_block = var.subnet_cidr_block

  availability_zone = "ap-northeast-2a"

  tags = {
    Name = "terraform-demo-subnet"
  }
}

resource "aws_internet_gateway" "myigw" {
  vpc_id = aws_vpc.myvpc.id

  tags = {
    Name = "terraform-demo-igw"
  }
}

resource "aws_route_table" "myrt" {
  vpc_id = aws_vpc.myvpc.id

  tags = {
    Name = "terraform-demo-rt"
  }
}

resource "aws_route_table_association" "myrtassociation1" {
  subnet_id      = aws_subnet.mysubnet.id
  route_table_id = aws_route_table.myrt.id
}

resource "aws_route" "mydefaultroute" {
  route_table_id         = aws_route_table.myrt.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.myigw.id
}

module-demo/0-vpc/variables.tf

variable "vpc_cidr_block" {
    description = "VPC CIDR block"
    type        = string
}

variable "subnet_cidr_block" {
    description = "SUBNET CIDR block"
    type        = string
}

module-demo/0-vpc/outputs.tf

output "vpc_id" {
  value = aws_vpc.myvpc.id
}

output "mysubnet_id" {
  value = aws_subnet.mysubnet.id
}

module-demo/1-sg/main.tf

resource "aws_security_group" "mysg" {
  vpc_id      = var.vpc_id
  name        = "terraform SG"
  description = "terraform demo SG"
}

resource "aws_security_group_rule" "mysginbound" {
  type              = "ingress"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.mysg.id
}

resource "aws_security_group_rule" "mysgoutbound" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.mysg.id
}

module-demo/1-sg/variables.tf

variable "vpc_id" {
  description = "The VPC ID where the security group will be created."
  type        = string
}

module-demo/1-sg/outputs.tf

output "security_group_id" {
  value = aws_security_group.mysg.id
}

module-demo/2-ec2/main.tf

data "aws_ami" "my_amazonlinux2" {
  most_recent = true
  filter {
    name   = "owner-alias"
    values = ["amazon"]
  }

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-ebs"]
  }

  owners = ["amazon"]
}

resource "aws_instance" "myec2" {

  ami                         = data.aws_ami.my_amazonlinux2.id
  associate_public_ip_address = true
  instance_type               = var.instance_type
  vpc_security_group_ids      = [var.security_group_id]
  subnet_id                   = var.mysubnet_id

  user_data = <<-EOF
              #!/bin/bash
              wget https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64
              mv busybox-x86_64 busybox
              chmod +x busybox
              echo "Web Server</h1>" > index.html
              nohup ./busybox httpd -f -p 80 &
              EOF

  user_data_replace_on_change = true

  tags = {
    Name = "terraform-demo-myec2"
  }
}

module-demo/2-ec2/variables.tf

variable "instance_type" {
  description = "The type of instance to start."
  type        = string
}

variable "security_group_id" {
  description = "The type of instance to start."
  type        = string
}

variable "mysubnet_id" {
  description = "subnet id"
  type        = string
}

module-demo/2-ec2/outputs.tf

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

배포

파일을 모두 작성하고 나서 아래의 명령어를 실행해서 배포할 수 있다.

terraform init &&
terraform plan &&
terraform apply -auto-approve

EC2 인스턴스가 생성되고 나서 약 2분 후에 아래의 명령어를 실행해서 접속을 시도해보자.

MYIP=$(echo module.ec2_instance.myec2_public_ip | terraform console | tr -d '"')
while true; do curl --connect-timeout 1  http://$MYIP/ ; echo "------------------------------"; date; sleep 1; done

정상적으로 설치가 되었다면 아래와 같이 출력될 것이다.

Web Server</h1>
------------------------------
Sat Apr 27 13:48:01 KST 2024
Web Server</h1>
------------------------------
Sat Apr 27 13:48:02 KST 2024
...

자원을 삭제하기 위해 아래의 명령어를 실행한다.

terraform destroy -auto-approve

Terraform + EKS 배포

테라폼을 이용해서 EKS 를 배포해보자.

코드는 스터디에서 제공해주신 것을 이용했다.

디렉토리 구조는 아래와 같다.

.
├── eks.tf
├── irsa.tf
├── variables.tf
└── vpc.tf

eks.tf

data "aws_caller_identity" "current" {}

resource "aws_iam_policy" "external_dns_policy" {
  name        = "${var.ClusterBaseName}ExternalDNSPolicy"
  description = "Policy for allowing ExternalDNS to modify Route 53 records"

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "route53:ChangeResourceRecordSets"
        ],
        "Resource": [
          "arn:aws:route53:::hostedzone/*"
        ]
      },
      {
        "Effect": "Allow",
        "Action": [
          "route53:ListHostedZones",
          "route53:ListResourceRecordSets"
        ],
        "Resource": [
          "*"
        ]
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "external_dns_policy_attach" {
  role       = "${var.ClusterBaseName}-node-group-eks-node-group"
  policy_arn = aws_iam_policy.external_dns_policy.arn

  depends_on = [module.eks]
}

resource "aws_security_group" "node_group_sg" {
  name        = "${var.ClusterBaseName}-node-group-sg"
  description = "Security group for EKS Node Group"
  vpc_id      = module.vpc.vpc_id

  tags = {
    Name = "${var.ClusterBaseName}-node-group-sg"
  }
}

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~>20.0"

  cluster_name = var.ClusterBaseName
  cluster_version = var.KubernetesVersion
  cluster_endpoint_private_access = false
  cluster_endpoint_public_access  = true

  cluster_addons = {
    coredns = {
      most_recent = true
    }
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent = true
    }
  }

  vpc_id = module.vpc.vpc_id
  enable_irsa = true
  subnet_ids = module.vpc.public_subnets

  eks_managed_node_groups = {
    default = {
      name             = "${var.ClusterBaseName}-node-group"
      use_name_prefix  = false
      instance_type    = var.WorkerNodeInstanceType
      desired_size     = var.WorkerNodeCount
      max_size         = var.WorkerNodeCount + 2
      min_size         = var.WorkerNodeCount - 1
      disk_size        = var.WorkerNodeVolumesize
      subnets          = module.vpc.public_subnets
      key_name         = var.KeyName
      vpc_security_group_ids = [aws_security_group.node_group_sg.id]
      iam_role_name    = "${var.ClusterBaseName}-node-group-eks-node-group"
      iam_role_use_name_prefix = false
      iam_role_additional_policies = {
        "${var.ClusterBaseName}ExternalDNSPolicy" = aws_iam_policy.external_dns_policy.arn
      }
   }
  }

  access_entries = {
    admin = {
      kubernetes_groups = []
      principal_arn     = "${data.aws_caller_identity.current.arn}"

      policy_associations = {
        myeks = {
          policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
          access_scope = {
            namespaces = []
            type       = "cluster"
          }
        }
      }
    }
  }

  tags = {
    Environment = "${var.ClusterBaseName}-lab"
    Terraform   = "true"
  }
}

irsa.tf

locals {
  cluster_oidc_issuer_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${replace(module.eks.cluster_oidc_issuer_url, "https://", "")}"
}

module "eks-external-dns" {
  source = "DNXLabs/eks-external-dns/aws"
  version = "0.2.0"

  cluster_name                     = var.ClusterBaseName
  cluster_identity_oidc_issuer     = module.eks.cluster_oidc_issuer_url
  cluster_identity_oidc_issuer_arn = local.cluster_oidc_issuer_arn

  enabled = false

}

module "aws_load_balancer_controller_irsa_role" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "5.3.1"

  role_name = "${var.ClusterBaseName}-aws-load-balancer-controller"

  attach_load_balancer_controller_policy = true

  oidc_providers = {
    ex = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:aws-load-balancer-controller"]
    }
  }
}

provider "kubernetes" {
  host                   = module.eks.cluster_endpoint
  token                  = data.aws_eks_cluster_auth.cluster.token
  cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
}

data "aws_eks_cluster" "cluster" {
  name = module.eks.cluster_name
  depends_on = [module.eks]
}

data "aws_eks_cluster_auth" "cluster" {
  name = module.eks.cluster_name
  depends_on = [module.eks]
}

resource "kubernetes_service_account" "aws_lb_controller" {
  metadata {
    name      = "aws-load-balancer-controller"
    namespace = "kube-system"
    annotations = {
      "eks.amazonaws.com/role-arn" = module.aws_load_balancer_controller_irsa_role.iam_role_arn
    }
  }
}

variables.tf

variable "KeyName" {
  description = "Name of an existing EC2 KeyPair to enable SSH access to the instances."
  type        = string
}

variable "ClusterBaseName" {
  description = "Base name of the cluster."
  type        = string
  default     = "myeks"
}

variable "KubernetesVersion" {
  description = "Kubernetes version for the EKS cluster."
  type        = string
  default     = "1.29"
}

variable "WorkerNodeInstanceType" {
  description = "EC2 instance type for the worker nodes."
  type        = string
  default     = "t3.medium"
}

variable "WorkerNodeCount" {
  description = "Number of worker nodes."
  type        = number
  default     = 3
}

variable "WorkerNodeVolumesize" {
  description = "Volume size for worker nodes (in GiB)."
  type        = number
  default     = 30
}

variable "TargetRegion" {
  description = "AWS region where the resources will be created."
  type        = string
  default     = "ap-northeast-2"
}

variable "availability_zones" {
  description = "List of availability zones."
  type        = list(string)
  default     = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
}

variable "VpcBlock" {
  description = "CIDR block for the VPC."
  type        = string
  default     = "192.168.0.0/16"
}

variable "public_subnet_blocks" {
  description = "List of CIDR blocks for the public subnets."
  type        = list(string)
  default     = ["192.168.1.0/24", "192.168.2.0/24", "192.168.3.0/24"]
}

variable "private_subnet_blocks" {
  description = "List of CIDR blocks for the private subnets."
  type        = list(string)
  default     = ["192.168.11.0/24", "192.168.12.0/24", "192.168.13.0/24"]
}

vpc.tf

provider "aws" {
  region = var.TargetRegion
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~>5.7"

  name = "${var.ClusterBaseName}-VPC"
  cidr = var.VpcBlock
  azs  = var.availability_zones

  enable_dns_support   = true
  enable_dns_hostnames = true

  public_subnets  = var.public_subnet_blocks
  private_subnets = var.private_subnet_blocks

  enable_nat_gateway = true
  single_nat_gateway = true
  one_nat_gateway_per_az = false
  
  map_public_ip_on_launch = true
 
  igw_tags = {
    "Name" = "${var.ClusterBaseName}-IGW"
  }
 
  nat_gateway_tags = {
    "Name" = "${var.ClusterBaseName}-NAT"
  }
 
  public_subnet_tags = {
    "Name"                     = "${var.ClusterBaseName}-PublicSubnet"
    "kubernetes.io/role/elb"   = "1"
  }

  private_subnet_tags = {
    "Name"                             = "${var.ClusterBaseName}-PrivateSubnet"
    "kubernetes.io/role/internal-elb" = "1"
  }

  tags = {
    "Environment" = "${var.ClusterBaseName}-lab"
  }
}

후기

8주라는 시간이 굉장히 빠르게 지나간 것 같다.

결코 쉽지 않은 내용이었는데, 끝까지 포기하지 않아서 뿌듯하고 기쁘다.

사실 스터디 1주차~2주차에는 스터디를 포기하려고 마음을 먹었었다.

취업을 준비하면서 사이드 프로젝트를 2개 함께 진행하고 있었는데, 스터디까지 참여하면서 많은 부하가 걸렸기 때문이다.

겨우 EKS 스터디 내용을 정리하면 웹 프로젝트를 하고, 웹 프로젝트 끝내면 쿠버네티스 프로젝트로 이어지는 정말 부담스러운 일정이었다.

1주일 중 하루라도 마음 편히 쉬었던 적이 손에 꼽을 정도로 심리적인 압박감과 스트레스가 상당했다.

누가 스터디 참여하라고 강요한 것도 아니었고, 스스로 선택한 험난한 길이었지만 이 선택을 결코 후회하지 않는다.

스터디 참여하기 전에 진행하고 있던 쿠버네티스 프로젝트를 위해 혼자 쿠버네티스를 학습하고 있었는데, 가이드라인을 찾지 못해서 많이 헤매고 있었다.

이번에 스터디를 참여하면서 다양한 실습을 해보고, 쿠버네티스 클러스터 환경 구성보다 쿠버네티스 작동 원리에 집중하다보니 그동안 부족하다고 느꼈던 부분을 많이 채울 수 있었다.

스터디 내용을 100% 이해했다고 말할 수 없지만, 쿠버네티스의 핵심 작동 원리를 이해하는 데 큰 도움이 되었다.

누군가 내게 쿠버네티스를 물어본다면 간략하게 설명할 수 있는 정도는 된 것 같다.

이 자리를 빌어 스터디를 이끌어 주신 가시다님과 스터디 운영을 도와주신 많은 분들께 감사함을 전하고 싶다.

참고자료

profile
성장하는 개발자, 한준혁입니다.

4개의 댓글

comment-user-thumbnail
2024년 5월 1일

스터디 모임장 가시다입니다.
8주 기간 동안 정말 고생하셨습니다.
블로그 글만 봐도 엄청나게 성장하셨네요.
그럼 추후에 또 다른 주제의 스터디에서 뵙겠습니다!

1개의 답글
comment-user-thumbnail
2024년 5월 2일

안녕하세요 이정훈(제리)입니다.
어려운 과정 완주하셔서 축하합니다. 제가 스터디 소개 드렸는데, 막 뿌듯하네요. ^^

1개의 답글