본문 바로가기

Newb/Terraform

Trraform으로 지정한 AWS 리소스 비용 계산 해보기

반응형
CloudNet@ T101 스터디 진행 후 작성하는 졸업과제 포스팅 입니다.

T101 스터디의 졸업 과제를 위해 어떤 포스팅을 해야 할지 고민 도중 유튜브에서 "비용을 잡아야 클라우드를 잡는다"라는 제목의 유튜브를 접하게 되었습니다.

 

해당 영상에서는 클라우드 환경에서 적합하지 않는 사양 등을 이용함으로써 낭비되는 비용이 약 20조 원의 규모로써 35%나 된다고 이야기를 하고 있습니다.

비용을 잡아야 클라우드를 잡는다

사실 클라우드의 사용 비용을 생각해보면 초기 투자 비용이 온프레미스보다 작기 때문에 싸게 보일수는 있지만, 제대로 된 아키텍처를 구성하지 않고 막상 사용을 해보면 클라우드 사용 비용도 만만치 않음을 깨닫게 됩니다.

 

그렇게 때문에 최근에는 클라우드의 비용을 관리하는 "Finance(재무)"와 "DevOps(운영)"의 합성어인 FinOps에도 주목되고 있습니다.

 

위와 같은 이유와 테스트를 할 지라도 내가 사용하고 있는 리소스의 금액을 알 수 있다면 좋을 것 같아 Terraform AWS 리소스 비용을 계산해주는 모듈을 테스트 해보기로 하였습니다.

 

여러 가지의 AWS 비용 계산 모듈들 중 AWS Pricing API를 사용하여 지정된 리소스 비용을 계산하는 모듈을 테스트해보겠습니다.


지원되는 기능

  • 생성 전 비용 계산(tfplan)
  • 생성 후 비용 계산(tfstate)
  • 여러 소스에서 비용 계산(로컬, 원격 상태, 지정된 리소스)
  • 선택적으로 cost.modules.tf 사용
  • Terraform을 실행할 수 있는 제한된 CI/CD 플랫폼에서 실행 가능

지원되는 리소스

  • EC2 인스턴스(온디맨드) 및 자동 확장 그룹(시작 구성 및 시작 템플릿)
    • aws_instance
  • EBS 볼륨, 스냅샷, 스냅샷 복사본
    • aws_ebs_볼륨
    • aws_ebs_스냅샷
    • aws_ebs_snapshot_copy
  • 탄력적 로드 밸런싱(ELB, ALB, NLB)
    • aws_elb
    • aws_alb / aws_lb
  • NAT 게이트웨이
    • aws_nat_gateway
  • Redshift 클러스터
    • aws_redshift_cluster

지원 사양

pricing 모듈을 사용하기 위한 지원 사양이므로, 아래의 조건은 충족되어야 사용이 가능합니다.

  1. Terraorm >= 1.0
  2. aws >= 4.0

pricing 모듈의 Outputs

Name Description
input_resources Map of input resource filters (from plan/state or static)
pricing_per_resources Map of resource pricing
pricing_product_filters Map of pricing product filters (as they are submitted using data source aws_pricing_product)
resource_quantity Map of resource quantity
resources Map of provided resources with filters
total_price_per_hour Total price for all resources
total_price_per_month Total price for all resources per month (730 hours)

사전 준비

해당 작업을 진행하기 전에 아래의 항목들이 사전적으로 작업돼야 합니다.

  1. Terraform 설치
  2. AWS CLI 설치
  3. AWS IAM 계정 세팅
  4. AWS CLI에 IAM 계정 세팅

pricing 모듈 가져오기

pricing 모듈은 아래의 github 주소에서 확인할 수 있습니다.

모듈 가져오기 전 디렉토리에 아무것도 없는 상태

test@test PC ~/pricing
$ ll
total 0

 

git clone 명령어로 pricing 모듈 소스 가져오기

test@test PC ~/pricing
$ git clone https://github.com/terraform-aws-modules/terraform-aws-pricing.git
Cloning into 'terraform-aws-pricing'...
remote: Enumerating objects: 286, done.
remote: Counting objects: 100% (93/93), done.
remote: Compressing objects: 100% (62/62), done.
remote: Total 286 (delta 48), reused 45 (delta 28), pack-reused 193
Receiving objects: 100% (286/286), 105.50 KiB | 8.79 MiB/s, done.
Resolving deltas: 100% (134/134), done.

 

가져온 모듈 확인

## 모듈 확인
test@test PC ~/pricing
$ ll
total 4
drwxr-xr-x 1 test 197121 0 Dec  8 16:44 terraform-aws-pricing/


## terraform-aws-pricing 디렉토리 안의 파일 확인
test@test PC ~/pricing/terraform-aws-pricing (master)
$ ll
total 28
-rw-r--r-- 1 test 197121  3780 Dec  8 16:44 CHANGELOG.md
-rw-r--r-- 1 test 197121 10349 Dec  8 16:44 LICENSE
-rw-r--r-- 1 test 197121  6810 Dec  8 16:44 README.md
drwxr-xr-x 1 test 197121     0 Dec  8 16:44 dev/
drwxr-xr-x 1 test 197121     0 Dec  8 16:44 examples/
drwxr-xr-x 1 test 197121     0 Dec  8 16:44 modules/

 

modules 디렉토리 남기고 제거하기

## 상위 디렉토리 이동
test@test PC ~/pricing/terraform-aws-pricing (master)
$ cd ..

## terraform-aws-pricing/modules 디렉토리를 상위 디렉토리로 이동
test@test PC ~/pricing
$ mv terraform-aws-pricing/modules/ ./

## terraform-aws-pricing 디렉토리 제거
test@test PC ~/pricing
$ rm -rf terraform-aws-pricing/

## 최종 moduels 디렉토리 확인
test@test PC ~/pricing
$ ll
total 0
drwxr-xr-x 1 test 197121 0 Dec  8 16:44 modules/

Terraform 구성

아래의 구성은 저의 주관적인 생각으로 관리하기 쉬울 것 같은 파일 구성으로 나눈 방식이므로, 다른 방식으로 구성하셔도 무방합니다.

 

versions.tf

pricing 모듈에 대한 지원 사양을 맞추기 위해 version.tf 파일에 Terraform과 AWS에 대한 버전을 적어줍니다.

terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 4.0"
    }
  }
}

 

main.tf

main.tf 파일에는 프로바이더에 대한 정보를 기입합니다.

pricing 모듈에서는 AWS 프로바이더는 항상 us-east-1 또는 ap-south-1 를 사용하여야 한다고 되어 있으므로 둘 중에 한 개를 골라 적어줍니다. 

provider "aws" {
  region = "us-east-1"
}

 

ec2.tf

해당 모듈은 지정된 리소스에 대하여 리소스 비용을 계산해주므로 모듈 안에서 생성할 리소스를 지정해줍니다.

module "pricing" {
  source = "./modules/pricing"

  debug_output      = true
  query_all_regions = false

  resources = {
    "aws_instance.ec2" = {
      instanceType = "t2.micro"
      location     = "ap-northeast-2"
    }
  }
}

 

outputs.tf

pricing 모듈 다음으로 중요한 outputs 파일입니다.

AWS 리소스 비용에 대한 결과를 출력해줄 outputs 파일이며, pricing 모듈에서 사용되는 outputs 파일들을 적어두었습니다.

output "resources" {
  description = "Map of provided resources with filters"
  value       = module.pricing.resources
}

output "input_resources" {
  description = "Map of input resource filters (from plan/state or static)"
  value       = module.pricing.input_resources
}

output "pricing_product_filters" {
  description = "Map of pricing product filters (as they are submitted using data source `aws_pricing_product`)"
  value       = module.pricing.pricing_product_filters
}

output "resource_quantity" {
  description = "Map of resource quantity"
  value       = module.pricing.resource_quantity
}

output "pricing_per_resources" {
  description = "Map of resource pricing"
  value       = module.pricing.pricing_per_resources
}

output "total_price_per_hour" {
  description = "Total price for all resources"
  value       = module.pricing.total_price_per_hour
}

output "total_price_per_month" {
  description = "Total price for all resources per month (730 hours)"
  value       = module.pricing.total_price_per_month
}

실행

terrform init

test@test PC ~/pricing
$ terraform.exe init
Initializing modules...
- pricing in modules\pricing

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching ">= 4.0.0"...
- Installing hashicorp/aws v4.45.0...
- Installed hashicorp/aws v4.45.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

 

terraform plan

terraform plan 시 outputs 파일에서 지정해두었던 가격에 대한 정보들이 출력되는 것을 확인할 수 있습니다.

 

  + pricing_per_resources   = {
      + "aws_instance.ec2" = 0.0144
    }

 

 + total_price_per_hour    = 0.0144
 + total_price_per_month   = 10.51

test@test PC ~/pricing
$ terraform.exe plan
module.pricing.data.aws_regions.all: Reading...
module.pricing.data.aws_regions.all: Read complete after 1s [id=aws]
module.pricing.data.aws_region.one["ap-southeast-2"]: Reading...
module.pricing.data.aws_region.one["eu-central-1"]: Reading...
module.pricing.data.aws_region.one["sa-east-1"]: Reading...
module.pricing.data.aws_region.one["eu-west-3"]: Reading...
module.pricing.data.aws_region.one["ap-southeast-1"]: Reading...
module.pricing.data.aws_region.one["ap-northeast-1"]: Reading...
module.pricing.data.aws_region.one["ap-south-1"]: Reading...
module.pricing.data.aws_region.one["eu-west-1"]: Reading...
module.pricing.data.aws_region.one["eu-north-1"]: Reading...
module.pricing.data.aws_region.one["us-east-2"]: Reading...
module.pricing.data.aws_region.one["ap-southeast-2"]: Read complete after 0s [id=ap-southeast-2]
module.pricing.data.aws_region.one["eu-central-1"]: Read complete after 0s [id=eu-central-1]
module.pricing.data.aws_region.one["sa-east-1"]: Read complete after 0s [id=sa-east-1]
module.pricing.data.aws_region.one["eu-west-3"]: Read complete after 0s [id=eu-west-3]
module.pricing.data.aws_region.one["eu-north-1"]: Read complete after 0s [id=eu-north-1]
module.pricing.data.aws_region.one["eu-west-1"]: Read complete after 0s [id=eu-west-1]
module.pricing.data.aws_region.one["ap-southeast-1"]: Read complete after 0s [id=ap-southeast-1]
module.pricing.data.aws_region.one["ap-south-1"]: Read complete after 0s [id=ap-south-1]
module.pricing.data.aws_region.one["ap-northeast-1"]: Read complete after 0s [id=ap-northeast-1]
module.pricing.data.aws_region.one["us-west-2"]: Reading...
module.pricing.data.aws_region.one["us-east-2"]: Read complete after 0s [id=us-east-2]
module.pricing.data.aws_region.one["ca-central-1"]: Reading...
module.pricing.data.aws_region.one["us-west-1"]: Reading...
module.pricing.data.aws_region.one["ap-northeast-2"]: Reading...
module.pricing.data.aws_region.one["us-east-1"]: Reading...
module.pricing.data.aws_region.one["eu-west-2"]: Reading...
module.pricing.data.aws_region.one["us-west-2"]: Read complete after 0s [id=us-west-2]
module.pricing.data.aws_region.one["ca-central-1"]: Read complete after 0s [id=ca-central-1]
module.pricing.data.aws_region.one["us-west-1"]: Read complete after 0s [id=us-west-1]
module.pricing.data.aws_region.one["us-east-1"]: Read complete after 0s [id=us-east-1]
module.pricing.data.aws_region.one["ap-northeast-2"]: Read complete after 0s [id=ap-northeast-2]
module.pricing.data.aws_region.one["eu-west-2"]: Read complete after 0s [id=eu-west-2]
module.pricing.data.aws_pricing_product.this["aws_instance.ec2"]: Reading...
module.pricing.data.aws_pricing_product.this["aws_instance.ec2"]: Read complete after 1s [id=2025191378]

Changes to Outputs:
  + input_resources         = {
      + "aws_instance.ec2" = {
          + instanceType = "t2.micro"
          + location     = "ap-northeast-2"
        }
    }
  + pricing_per_resources   = {
      + "aws_instance.ec2" = 0.0144
    }
  + pricing_product_filters = {
      + "aws_instance.ec2" = {
          + filters      = {
              + capacitystatus  = "Used"
              + instanceType    = "t2.micro"
              + licenseModel    = "No License required"
              + location        = "Asia Pacific (Seoul)"
              + operatingSystem = "Linux"
              + preInstalledSw  = "NA"
              + tenancy         = "Shared"
            }
          + service_code = "AmazonEC2"
        }
    }
  + resource_quantity       = {
      + "aws_instance.ec2" = 1
    }
  + resources               = {
      + "aws_instance.ec2" = {
          + capacitystatus  = "Used"
          + instanceType    = "t2.micro"
          + licenseModel    = "No License required"
          + location        = "Asia Pacific (Seoul)"
          + operatingSystem = "Linux"
          + preInstalledSw  = "NA"
          + tenancy         = "Shared"
        }
    }
  + total_price_per_hour    = 0.0144
  + total_price_per_month   = 10.51

You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

 

terraform apply

apply를 하면 내가 지정한 outputs들의 값이 출력되는 것을 확인 할 수 있습니다.

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

Outputs:

input_resources = tomap({
  "aws_instance.ec2" = {
    "instanceType" = "t2.micro"
    "location" = "ap-northeast-2"
  }
})
pricing_per_resources = {
  "aws_instance.ec2" = 0.0144
}
pricing_product_filters = tomap({
  "aws_instance.ec2" = {
    "filters" = {
      "capacitystatus" = "Used"
      "instanceType" = "t2.micro"
      "licenseModel" = "No License required"
      "location" = "Asia Pacific (Seoul)"
      "operatingSystem" = "Linux"
      "preInstalledSw" = "NA"
      "tenancy" = "Shared"
    }
    "service_code" = "AmazonEC2"
  }
})
resource_quantity = {
  "aws_instance.ec2" = 1
}
resources = tomap({
  "aws_instance.ec2" = {
    "capacitystatus" = "Used"
    "instanceType" = "t2.micro"
    "licenseModel" = "No License required"
    "location" = "Asia Pacific (Seoul)"
    "operatingSystem" = "Linux"
    "preInstalledSw" = "NA"
    "tenancy" = "Shared"
  }
})
total_price_per_hour = 0.0144
total_price_per_month = 10.51

 

비용 확인

비용 확인 시 실제로 온디맨드의 요금과 일치하는 것을 확인할 수 있습니다.

AWS 계산기 t2.micro 비용


ec2.tf 파일에서 ec2 추가 후 비용 확인 테스트

변경된 ec2.tf

t2.micro 타입의 ec2의 수를 1개에서 2개로 증가

module "pricing" {
  source = "./modules/pricing"

  debug_output      = true
  query_all_regions = false

  resources = {
    "aws_instance.ec2_1" = {
      instanceType = "t2.micro"
      location     = "ap-northeast-2"
    }

    "aws_instance.ec2_2" = {
      instanceType = "t2.micro"
      location     = "ap-northeast-2"
    }
  }
}

 

terraform plan

변경 된 내용

비용이 t2.micro 인스턴스가 1개에서 2개로 변경됨에 따라 비용도 2배로 증가한 것을 확인할 수 있습니다.

  ~ pricing_per_resources   = {
      - "aws_instance.ec2"   = 0.0144 -> null
      + "aws_instance.ec2_1" = 0.0144
      + "aws_instance.ec2_2" = 0.0144
    }

~ total_price_per_hour    = 0.0144 -> 0.0288
 ~ total_price_per_month   = 10.51 -> 21.02

test@test PC ~/pricing
$ terraform.exe plan
module.pricing.data.aws_regions.all: Reading...
module.pricing.data.aws_regions.all: Read complete after 1s [id=aws]
module.pricing.data.aws_region.one["ap-southeast-1"]: Reading...
module.pricing.data.aws_region.one["us-east-1"]: Reading...
module.pricing.data.aws_region.one["ap-southeast-2"]: Reading...
module.pricing.data.aws_region.one["eu-north-1"]: Reading...
module.pricing.data.aws_region.one["us-west-1"]: Reading...
module.pricing.data.aws_region.one["ap-northeast-2"]: Reading...
module.pricing.data.aws_region.one["ap-northeast-1"]: Reading...
module.pricing.data.aws_region.one["eu-west-3"]: Reading...
module.pricing.data.aws_region.one["eu-central-1"]: Reading...
module.pricing.data.aws_region.one["eu-central-1"]: Read complete after 0s [id=eu-central-1]
module.pricing.data.aws_region.one["ap-northeast-1"]: Read complete after 0s [id=ap-northeast-1]
module.pricing.data.aws_region.one["us-east-1"]: Read complete after 0s [id=us-east-1]
module.pricing.data.aws_region.one["eu-north-1"]: Read complete after 0s [id=eu-north-1]
module.pricing.data.aws_region.one["us-west-1"]: Read complete after 0s [id=us-west-1]
module.pricing.data.aws_region.one["ap-southeast-1"]: Read complete after 0s [id=ap-southeast-1]
module.pricing.data.aws_region.one["ap-southeast-2"]: Read complete after 0s [id=ap-southeast-2]
module.pricing.data.aws_region.one["ap-northeast-2"]: Read complete after 0s [id=ap-northeast-2]
module.pricing.data.aws_region.one["sa-east-1"]: Reading...
module.pricing.data.aws_region.one["eu-west-3"]: Read complete after 0s [id=eu-west-3]
module.pricing.data.aws_region.one["eu-west-2"]: Reading...
module.pricing.data.aws_region.one["ap-south-1"]: Reading...
module.pricing.data.aws_region.one["us-east-2"]: Reading...
module.pricing.data.aws_region.one["eu-west-1"]: Reading...
module.pricing.data.aws_region.one["ca-central-1"]: Reading...
module.pricing.data.aws_region.one["sa-east-1"]: Read complete after 0s [id=sa-east-1]
module.pricing.data.aws_region.one["ca-central-1"]: Read complete after 0s [id=ca-central-1]
module.pricing.data.aws_region.one["eu-west-2"]: Read complete after 0s [id=eu-west-2]
module.pricing.data.aws_region.one["ap-south-1"]: Read complete after 0s [id=ap-south-1]
module.pricing.data.aws_region.one["eu-west-1"]: Read complete after 0s [id=eu-west-1]
module.pricing.data.aws_region.one["us-east-2"]: Read complete after 0s [id=us-east-2]
module.pricing.data.aws_region.one["us-west-2"]: Reading...
module.pricing.data.aws_region.one["us-west-2"]: Read complete after 0s [id=us-west-2]
module.pricing.data.aws_pricing_product.this["aws_instance.ec2_1"]: Reading...
module.pricing.data.aws_pricing_product.this["aws_instance.ec2_2"]: Reading...
module.pricing.data.aws_pricing_product.this["aws_instance.ec2_1"]: Read complete after 1s [id=2025191378]
module.pricing.data.aws_pricing_product.this["aws_instance.ec2_2"]: Read complete after 1s [id=2025191378]

Changes to Outputs:
  ~ input_resources         = {
      - "aws_instance.ec2"   = {
          - instanceType = "t2.micro"
          - location     = "ap-northeast-2"
        } -> null
      + "aws_instance.ec2_1" = {
          + instanceType = "t2.micro"
          + location     = "ap-northeast-2"
        }
      + "aws_instance.ec2_2" = {
          + instanceType = "t2.micro"
          + location     = "ap-northeast-2"
        }
    }
  ~ pricing_per_resources   = {
      - "aws_instance.ec2"   = 0.0144 -> null
      + "aws_instance.ec2_1" = 0.0144
      + "aws_instance.ec2_2" = 0.0144
    }
  ~ pricing_product_filters = {
      - "aws_instance.ec2"   = {
          - filters      = {
              - capacitystatus  = "Used"
              - instanceType    = "t2.micro"
              - licenseModel    = "No License required"
              - location        = "Asia Pacific (Seoul)"
              - operatingSystem = "Linux"
              - preInstalledSw  = "NA"
              - tenancy         = "Shared"
            }
          - service_code = "AmazonEC2"
        } -> null
      + "aws_instance.ec2_1" = {
          + filters      = {
              + capacitystatus  = "Used"
              + instanceType    = "t2.micro"
              + licenseModel    = "No License required"
              + location        = "Asia Pacific (Seoul)"
              + operatingSystem = "Linux"
              + preInstalledSw  = "NA"
              + tenancy         = "Shared"
            }
          + service_code = "AmazonEC2"
        }
      + "aws_instance.ec2_2" = {
          + filters      = {
              + capacitystatus  = "Used"
              + instanceType    = "t2.micro"
              + licenseModel    = "No License required"
              + location        = "Asia Pacific (Seoul)"
              + operatingSystem = "Linux"
              + preInstalledSw  = "NA"
              + tenancy         = "Shared"
            }
          + service_code = "AmazonEC2"
        }
    }
  ~ resource_quantity       = {
      - "aws_instance.ec2"   = 1 -> null
      + "aws_instance.ec2_1" = 1
      + "aws_instance.ec2_2" = 1
    }
  ~ resources               = {
      - "aws_instance.ec2"   = {
          - capacitystatus  = "Used"
          - instanceType    = "t2.micro"
          - licenseModel    = "No License required"
          - location        = "Asia Pacific (Seoul)"
          - operatingSystem = "Linux"
          - preInstalledSw  = "NA"
          - tenancy         = "Shared"
        } -> null
      + "aws_instance.ec2_1" = {
          + capacitystatus  = "Used"
          + instanceType    = "t2.micro"
          + licenseModel    = "No License required"
          + location        = "Asia Pacific (Seoul)"
          + operatingSystem = "Linux"
          + preInstalledSw  = "NA"
          + tenancy         = "Shared"
        }
      + "aws_instance.ec2_2" = {
          + capacitystatus  = "Used"
          + instanceType    = "t2.micro"
          + licenseModel    = "No License required"
          + location        = "Asia Pacific (Seoul)"
          + operatingSystem = "Linux"
          + preInstalledSw  = "NA"
          + tenancy         = "Shared"
        }
    }
  ~ total_price_per_hour    = 0.0144 -> 0.0288
  ~ total_price_per_month   = 10.51 -> 21.02

You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

결론

테스트를 진행한 방법은 단순히 내가 테라폼에서 지정한 리소스에 대하여 비용을 산출해주기 때문에 테라폼으로 생성하기 전에 비용을 체크하는 용도 정도로만 사용이 가능하지만,

아래의 Youtube 링크와 같이 terraform cloud, CI/CD 등과 연동하여 배포를 할 때, 배포되는 리소스에 대하여 비용을 비교도 할 수 있고, 비용이 초과하면 배포가 되지 않도록 제한을 할 수도 있어 매우 유용할 것으로 보입니다.


참고자료

반응형