EKS 스터디 마지막 주차는 인프라를 코드로 관리하는 IaC(Infrastructure as Code)를 학습했다.
그 중에서도 Terraform 의 기본적인 사용법부터 EKS 클러스터를 설치하는 방법까지 전반적으로 살펴보았다.
EKS 나 쿠버네티스에 대한 이해 없이 테라폼을 바로 사용하기에는 학습 곡선이 꽤나 가파르기 때문에 먼저 쿠버네티스를 이해하고 사용할 것을 권장한다.
출처: 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
.tf
를 사용하며, HCL(HashiCorp Configuration Language)라는 언어로 작성한다. Terraform 은 로컬 환경에서 클라우드 서비스를 이용할 수 있다.
MacOS 에서 설치했으며, 패키지 관리는 homebrew 를 이용했다.
공식 문서에서 설명하는 대로 설치를 진행한다. (링크)
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 이다.
필수로 설치할 필요는 없는 것 같지만, 다른 버전을 바꿔서 사용한다면 반드시 설치하자.
이미 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 에 인프라를 구성하기 위해 AWS CLI 를 설치했다.
brew install awscli
설치 이후 aws configure
명령어를 실행해서 자격 증명을 해주어야 한다.
자격 증명에 대한 설명은 생략한다.
AWS EKS 를 이용하기 위해 eksctl 을 설치했다.
brew install eksctl
eksctl 과 마찬가지로 쿠버네티스 API 서버를 이용하기 위해 kubectl 을 설치했다.
brew install kubernetes-cli
EKS 에 패키지를 간편하게 설치하기 위한 helm 를 설치했다.
brew install helm
실습을 편리하게 진행하기 위한 툴도 함께 설치했다.
brew install tree jq watch
테라폼 파일을 작성할 때 HCL 문법에 맞게 하이라이팅을 해주는 Extension 을 설치했다.
실습을 위한 폴더를 하나 만들어서 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 문법을 간단하게 정리하면 아래와 같다.
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 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 하겠다는 출력이 나왔다.
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
apply 명령어와 마찬가지로 yes 를 입력해야 삭제가 된다.
자동으로 삭제하고 싶다면 -auto-approve
옵션을 추가하면 된다.
terraform destroy -auto-approve
위에서 언급했듯 HCL 은 코드를 통해 인프라를 관리할 수 있는 문법이다.
일반적인 프로그래밍 언어처럼 조건문을 사용하거나 변수와 문자열을 함께 사용하는 String Interpolation 을 지원한다.
const name = "Jay";
console.log(`Hello World, ${Jay}`); // String Interpolation
${Jay}
는 변수인데, 위와 같이 작성하면 변수를 문자열로 변환해서 사용할 수 있게 된다.동일한 내용을 JSON 으로 표현하는 것보다 간결하고 읽기 쉽게 작성할 수 있다.
비교를 위해 HCL 을 이용한 테라폼 구성과 JSON 을 이용한 CloudFormation 구성을 비교해보자.
resource "local_file" "abc" {
content = "abc!"
filename = "${path.module}/abc.txt"
metadata = {
name = "{$var.PilotServerName}-vm"
}
}
{
"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
}
테라폼에서는 resource, data, output, variable 와 같은 블럭들로 코드를 구성할 수 있다.
파일마다 한 가지 블록만 작성하도록 하는 것이 유지보수 측면에서 권장된다.
규모가 작은 프로젝트라면 하나의 파일에 합쳐서 작성할 수도 있지만, 확장성과 가독성을 위해서는 분리해서 작성하는 것이 낫다.
예를 들면, 아래와 같이 파일을 분리해서 작성할 수 있다.
.
├── main.tf
├── variables.tf
├── outputs.tf
├── <resource_name>.tf
└── data.tf
main.tf
는 terraform apply
명령어를 실행하면 가장 먼저 실행하는 파일이다.
테라폼 버전이나 프로바이더 버전은 자동으로 설정되지만, 다른 사람들과 함께 작업할 때는 버전을 명시적으로 선언하고, 필요한 조건을 입력해서 실행 오류를 최소화 하는 거싱 권장된다.
이는 오늘 실행한 결과와 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 방식을 따른다.
버전 제약 구문은 아래와 같은 규칙을 따른다.
=
또는 연산자 없음: 지정된 버전만을 허용. 다른 버전과 함께 사용 불가.!=
: 지정한 버전을 제외.>
, >=
, <
, <=
: 지정한 버전과 비교해 조건에 맞는 경우만 허용.~>
: 지정한 버전에서 가장 자리수가 낮은 구성요소만 증가하는 것을 허용.~> 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 블록이 표시된다.
프로바이더가 제공하는 리소스의 타입이다.
예를 들면, AWS 는 aws_instnace
라는 타입을 제공하는데, 이를 이용해서 EC2 인스턴스를 생성하고 조작하는 것이 가능하다.
resource "aws_instance" "example" {
ami = "ami-0217b147346e48e84"
instance_type = "t2.micro"
}
리소스는 아래의 동작들을 정의할 수 있다.
테라폼 종속성은 자동으로 연관 관계가 정의되지만, 명시적으로 종속성을 부여하고자 할 때는 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!
을 가져온다. 이때, 리소스 def
는 abc
가 먼저 만들어져야 하기 때문에 abc
에 종속성이 생긴다.
terraform apply
명령어를 실행하면 실제로 def.txt
는 abc.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 블록을 이용하면 변수를 정의할 수 있다.
타입과 기본 값을 지정할 수 있으며, 타입에는 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
를 이용하는 것을 권장한다.
변수 정의할 때 사용할 수 있는 메타인수는 아래와 같다.
다른 자료형들은 아래와 같이 사용할 수 있다.
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
파일에는 .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
어떤 값을 내보낼 지 정의하는 블록이다.
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 "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.tf | resource, 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
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)
파일에 정의한 출력값을 의미한다.
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
}
variable "vpc_cidr_block" {
description = "VPC CIDR block"
type = string
}
variable "subnet_cidr_block" {
description = "SUBNET CIDR block"
type = string
}
output "vpc_id" {
value = aws_vpc.myvpc.id
}
output "mysubnet_id" {
value = aws_subnet.mysubnet.id
}
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
}
variable "vpc_id" {
description = "The VPC ID where the security group will be created."
type = string
}
output "security_group_id" {
value = aws_security_group.mysg.id
}
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"
}
}
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
}
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
테라폼을 이용해서 EKS 를 배포해보자.
코드는 스터디에서 제공해주신 것을 이용했다.
디렉토리 구조는 아래와 같다.
.
├── eks.tf
├── irsa.tf
├── variables.tf
└── vpc.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"
}
}
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
}
}
}
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"]
}
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% 이해했다고 말할 수 없지만, 쿠버네티스의 핵심 작동 원리를 이해하는 데 큰 도움이 되었다.
누군가 내게 쿠버네티스를 물어본다면 간략하게 설명할 수 있는 정도는 된 것 같다.
이 자리를 빌어 스터디를 이끌어 주신 가시다님과 스터디 운영을 도와주신 많은 분들께 감사함을 전하고 싶다.
스터디 모임장 가시다입니다.
8주 기간 동안 정말 고생하셨습니다.
블로그 글만 봐도 엄청나게 성장하셨네요.
그럼 추후에 또 다른 주제의 스터디에서 뵙겠습니다!