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 모듈을 사용하기 위한 지원 사양이므로, 아래의 조건은 충족되어야 사용이 가능합니다.
- Terraorm >= 1.0
- 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) |
사전 준비
해당 작업을 진행하기 전에 아래의 항목들이 사전적으로 작업돼야 합니다.
- Terraform 설치
- AWS CLI 설치
- AWS IAM 계정 세팅
- 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
비용 확인
비용 확인 시 실제로 온디맨드의 요금과 일치하는 것을 확인할 수 있습니다.
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 등과 연동하여 배포를 할 때, 배포되는 리소스에 대하여 비용을 비교도 할 수 있고, 비용이 초과하면 배포가 되지 않도록 제한을 할 수도 있어 매우 유용할 것으로 보입니다.
참고자료
'Newb > Terraform' 카테고리의 다른 글
Terraform으로 AWS에서 많이 사용하는 기본 서비스 구성 구축해보기 (0) | 2022.11.13 |
---|---|
Terraform 기본 명령어 & Flow (0) | 2022.11.08 |
Terraform ? (0) | 2022.10.30 |