Skip to content

Commit 7934bb7

Browse files
committed
feat(azure): Add MySQL Flexible Server resources
1 parent 5bf1e51 commit 7934bb7

File tree

9 files changed

+409
-26
lines changed

9 files changed

+409
-26
lines changed

infracost-usage-example.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,9 @@ resource_usage:
948948
long_term_retention_storage_gb: 1000 # Number of GBs used by long-term retention backup storage.
949949
extra_data_storage_gb: 250 # Override number of GBs used by extra data storage.
950950

951+
azurerm_mysql_flexible_server.my_flexible_server:
952+
additional_backup_storage_gb: 5000 # Additional backup storage in GB. If geo-redundancy is enabled, you should set this to twice the required storage capacity.
953+
951954
azurerm_mysql_server.my_server:
952955
additional_backup_storage_gb: 2000 # Additional consumption of backup storage in GB.
953956

@@ -958,7 +961,7 @@ resource_usage:
958961
monthly_p2s_connections_hrs: 2000 # Monthly connection hours to the point to site gateway
959962

960963
azurerm_postgresql_flexible_server.my_flexible_server:
961-
additional_backup_storage_gb: 5000 # Additional consumption of backup storage in GB.
964+
additional_backup_storage_gb: 5000 # Additional backup storage in GB. If geo-redundancy is enabled, you should set this to twice the required storage capacity.
962965

963966
azurerm_postgresql_server.my_server:
964967
additional_backup_storage_gb: 3000 # Additional consumption of backup storage in GB.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package azure
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
7+
"github.com/infracost/infracost/internal/resources/azure"
8+
"github.com/infracost/infracost/internal/schema"
9+
log "github.com/sirupsen/logrus"
10+
)
11+
12+
func getMySQLFlexibleServerRegistryItem() *schema.RegistryItem {
13+
return &schema.RegistryItem{
14+
Name: "azurerm_mysql_flexible_server",
15+
RFunc: newMySQLFlexibleServer,
16+
}
17+
}
18+
19+
func newMySQLFlexibleServer(d *schema.ResourceData, u *schema.UsageData) *schema.Resource {
20+
region := d.Get("location").String()
21+
sku := d.Get("sku_name").String()
22+
storage := d.GetInt64OrDefault("storage.0.size_gb", 0)
23+
iops := d.GetInt64OrDefault("storage.0.iops", 0)
24+
25+
var tier, size, version string
26+
27+
s := strings.Split(sku, "_")
28+
if len(s) < 3 || len(s) > 4 {
29+
log.Warnf("Unrecognised MySQL Flexible Server SKU format for resource %s: %s", d.Address, sku)
30+
return nil
31+
}
32+
33+
if len(s) > 2 {
34+
tier = strings.ToLower(s[0])
35+
size = s[2]
36+
}
37+
38+
if len(s) > 3 {
39+
version = s[3]
40+
}
41+
42+
supportedTiers := []string{"b", "gp", "mo"}
43+
if !contains(supportedTiers, tier) {
44+
log.Warnf("Unrecognised MySQL Flexible Server tier prefix for resource %s: %s", d.Address, sku)
45+
return nil
46+
}
47+
48+
if tier != "b" {
49+
coreRegex := regexp.MustCompile(`(\d+)`)
50+
match := coreRegex.FindStringSubmatch(size)
51+
if len(match) < 1 {
52+
log.Warnf("Unrecognised MySQL Flexible Server size for resource %s: %s", d.Address, sku)
53+
return nil
54+
}
55+
}
56+
57+
r := &azure.MySQLFlexibleServer{
58+
Address: d.Address,
59+
Region: region,
60+
SKU: sku,
61+
Tier: tier,
62+
InstanceType: size,
63+
InstanceVersion: version,
64+
Storage: storage,
65+
IOPS: iops,
66+
}
67+
r.PopulateUsage(u)
68+
69+
return r.BuildResource()
70+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package azure_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/infracost/infracost/internal/providers/terraform/tftest"
7+
)
8+
9+
func TestMySQLFlexibleServer(t *testing.T) {
10+
t.Parallel()
11+
if testing.Short() {
12+
t.Skip("skipping test in short mode")
13+
}
14+
15+
tftest.GoldenFileResourceTests(t, "mysql_flexible_server_test")
16+
}

internal/providers/terraform/azure/registry.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ var ResourceRegistry []*schema.RegistryItem = []*schema.RegistryItem{
103103
GetAzureRMWindowsVirtualMachineScaleSetRegistryItem(),
104104
getAzureRMVPNGatewayRegistryItem(),
105105
getAzureRMVPNGatewayConnectionRegistryItem(),
106+
getMySQLFlexibleServerRegistryItem(),
106107
}
107108

108109
// FreeResources grouped alphabetically
@@ -200,6 +201,9 @@ var FreeResources = []string{
200201
"azurerm_mariadb_firewall_rule",
201202
"azurerm_mariadb_virtual_network_rule",
202203
"azurerm_mysql_firewall_rule",
204+
"azurerm_mysql_flexible_database",
205+
"azurerm_mysql_flexible_server_configuration",
206+
"azurerm_mysql_flexible_server_firewall_rule",
203207
"azurerm_mysql_virtual_network_rule",
204208
"azurerm_postgresql_configuration",
205209
"azurerm_postgresql_firewall_rule",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
2+
Name Monthly Qty Unit Monthly Cost
3+
4+
azurerm_mysql_flexible_server.burstable
5+
├─ Compute (B_Standard_B1ms) 730 hours $16.06
6+
├─ Storage 30 GB $4.14
7+
└─ Additional backup storage 1,000 GB $95.00
8+
9+
azurerm_mysql_flexible_server.gp
10+
├─ Compute (GP_Standard_D4ds_v4) 730 hours $284.70
11+
├─ Storage 20 GB $2.76
12+
└─ Additional backup storage 2,000 GB $190.00
13+
14+
azurerm_mysql_flexible_server.mo
15+
├─ Compute (MO_Standard_E4ds_v4) 730 hours $382.52
16+
├─ Storage 20 GB $2.76
17+
├─ Additional IOPS 140 IOPS $8.40
18+
└─ Additional backup storage 3,000 GB $285.00
19+
20+
azurerm_mysql_flexible_server.non_usage_gp
21+
├─ Compute (GP_Standard_D16ds_v4) 730 hours $1,138.80
22+
├─ Storage 20 GB $2.76
23+
└─ Additional backup storage Monthly cost depends on usage: $0.095 per GB
24+
25+
OVERALL TOTAL $2,412.90
26+
──────────────────────────────────
27+
5 cloud resources were detected:
28+
∙ 4 were estimated, all of which include usage-based costs, see https://infracost.io/usage-file
29+
∙ 1 was free:
30+
∙ 1 x azurerm_resource_group
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
provider "azurerm" {
2+
skip_provider_registration = true
3+
features {}
4+
}
5+
6+
resource "azurerm_resource_group" "example" {
7+
name = "example-resources"
8+
location = "westus"
9+
}
10+
11+
resource "azurerm_mysql_flexible_server" "gp" {
12+
name = "example-mysqlflexibleserver"
13+
resource_group_name = azurerm_resource_group.example.name
14+
location = azurerm_resource_group.example.location
15+
16+
sku_name = "GP_Standard_D4ds_v4"
17+
}
18+
19+
resource "azurerm_mysql_flexible_server" "mo" {
20+
name = "example-mysqlflexibleserver"
21+
resource_group_name = azurerm_resource_group.example.name
22+
location = azurerm_resource_group.example.location
23+
24+
storage {
25+
iops = 500
26+
size_gb = 20
27+
}
28+
29+
sku_name = "MO_Standard_E4ds_v4"
30+
}
31+
32+
resource "azurerm_mysql_flexible_server" "burstable" {
33+
name = "example-mysqlflexibleserver"
34+
resource_group_name = azurerm_resource_group.example.name
35+
location = azurerm_resource_group.example.location
36+
37+
storage {
38+
iops = 360
39+
size_gb = 30
40+
}
41+
42+
sku_name = "B_Standard_B1ms"
43+
}
44+
45+
resource "azurerm_mysql_flexible_server" "non_usage_gp" {
46+
name = "example-mysqlflexibleserver"
47+
resource_group_name = azurerm_resource_group.example.name
48+
location = azurerm_resource_group.example.location
49+
50+
sku_name = "GP_Standard_D16ds_v4"
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 0.1
2+
resource_usage:
3+
azurerm_mysql_flexible_server.burstable:
4+
additional_backup_storage_gb: 1000
5+
6+
azurerm_mysql_flexible_server.gp:
7+
additional_backup_storage_gb: 2000
8+
9+
azurerm_mysql_flexible_server.mo:
10+
additional_backup_storage_gb: 3000
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package azure
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/infracost/infracost/internal/resources"
7+
"github.com/infracost/infracost/internal/schema"
8+
"github.com/shopspring/decimal"
9+
)
10+
11+
// MySQLFlexibleServer struct represents Azure MySQL Flexible Server resource.
12+
//
13+
// Resource information: https://docs.microsoft.com/en-gb/azure/mysql/flexible-server/
14+
// Pricing information: https://azure.microsoft.com/en-gb/pricing/details/mysql/flexible-server/
15+
type MySQLFlexibleServer struct {
16+
Address string
17+
Region string
18+
19+
SKU string
20+
Tier string
21+
InstanceType string
22+
InstanceVersion string
23+
Storage int64
24+
IOPS int64
25+
26+
// "usage" args
27+
AdditionalBackupStorageGB *float64 `infracost_usage:"additional_backup_storage_gb"`
28+
}
29+
30+
// MySQLFlexibleServerUsageSchema defines a list which represents the usage schema of MySQLFlexibleServer.
31+
var MySQLFlexibleServerUsageSchema = []*schema.UsageItem{
32+
{Key: "additional_backup_storage_gb", DefaultValue: 0, ValueType: schema.Float64},
33+
}
34+
35+
// PopulateUsage parses the u schema.UsageData into the MySQLFlexibleServer.
36+
// It uses the `infracost_usage` struct tags to populate data into the MySQLFlexibleServer.
37+
func (r *MySQLFlexibleServer) PopulateUsage(u *schema.UsageData) {
38+
resources.PopulateArgsWithUsage(r, u)
39+
}
40+
41+
// BuildResource builds a schema.Resource from a valid MySQLFlexibleServer struct.
42+
// This method is called after the resource is initialised by an IaC provider.
43+
// See providers folder for more information.
44+
func (r *MySQLFlexibleServer) BuildResource() *schema.Resource {
45+
costComponents := []*schema.CostComponent{
46+
r.computeCostComponent(),
47+
r.storageCostComponent(),
48+
}
49+
50+
if iopsCostComponent := r.iopsCostComponent(); iopsCostComponent != nil {
51+
costComponents = append(costComponents, iopsCostComponent)
52+
}
53+
54+
costComponents = append(costComponents, r.backupCostComponent())
55+
56+
return &schema.Resource{
57+
Name: r.Address,
58+
UsageSchema: MySQLFlexibleServerUsageSchema,
59+
CostComponents: costComponents,
60+
}
61+
}
62+
63+
// computeCostComponent returns a cost component for server compute requirements.
64+
func (r *MySQLFlexibleServer) computeCostComponent() *schema.CostComponent {
65+
attrs := getFlexibleServerFilterAttributes(r.Tier, r.InstanceType, r.InstanceVersion)
66+
67+
// Eds series has two spaces in CPAPI hence '\s+'
68+
productNameRegex := fmt.Sprintf("^Azure Database for MySQL Flexible Server %s\\s+%s", attrs.TierName, attrs.Series)
69+
70+
return &schema.CostComponent{
71+
Name: fmt.Sprintf("Compute (%s)", r.SKU),
72+
Unit: "hours",
73+
UnitMultiplier: decimal.NewFromInt(1),
74+
HourlyQuantity: decimalPtr(decimal.NewFromInt(1)),
75+
ProductFilter: &schema.ProductFilter{
76+
VendorName: strPtr("azure"),
77+
Region: strPtr(r.Region),
78+
Service: strPtr("Azure Database for MySQL"),
79+
ProductFamily: strPtr("Databases"),
80+
AttributeFilters: []*schema.AttributeFilter{
81+
{Key: "productName", ValueRegex: regexPtr(productNameRegex)},
82+
{Key: "skuName", ValueRegex: regexPtr(fmt.Sprintf("^%s$", attrs.SKUName))},
83+
{Key: "meterName", ValueRegex: regexPtr(fmt.Sprintf("^%s$", attrs.MeterName))},
84+
},
85+
},
86+
PriceFilter: &schema.PriceFilter{
87+
PurchaseOption: strPtr("Consumption"),
88+
},
89+
}
90+
}
91+
92+
// storageCostComponent returns a cost component for server's storage. If
93+
// storage is not defined, it is assumed it is a minimum default of 20GB.
94+
func (r *MySQLFlexibleServer) storageCostComponent() *schema.CostComponent {
95+
storage := r.Storage
96+
if storage == 0 {
97+
storage = 20 // minimum default
98+
}
99+
100+
return &schema.CostComponent{
101+
Name: "Storage",
102+
Unit: "GB",
103+
UnitMultiplier: decimal.NewFromInt(1),
104+
MonthlyQuantity: decimalPtr(decimal.NewFromInt(storage)),
105+
ProductFilter: &schema.ProductFilter{
106+
VendorName: strPtr("azure"),
107+
Region: strPtr(r.Region),
108+
Service: strPtr("Azure Database for MySQL"),
109+
ProductFamily: strPtr("Databases"),
110+
AttributeFilters: []*schema.AttributeFilter{
111+
{Key: "productName", Value: strPtr("Azure Database for MySQL Flexible Server Storage")},
112+
{Key: "meterName", Value: strPtr("Storage Data Stored")},
113+
},
114+
},
115+
}
116+
}
117+
118+
// iopsCostComponent returns a cost component for additional IOPS. Each server
119+
// includes free 300 IOPS and 3 IOPS per each storage GB. As minimum storage is
120+
// 20GB, the total free IOPS is 360. If no IOPS is defined it's assumed it is
121+
// the minimum of 360.
122+
func (r *MySQLFlexibleServer) iopsCostComponent() *schema.CostComponent {
123+
var freeIOPS int64 = 360
124+
125+
iops := r.IOPS
126+
if iops == 0 {
127+
iops = freeIOPS
128+
}
129+
130+
additionalIOPS := iops - freeIOPS
131+
132+
if additionalIOPS <= 0 {
133+
return nil
134+
}
135+
136+
return &schema.CostComponent{
137+
Name: "Additional IOPS",
138+
Unit: "IOPS",
139+
UnitMultiplier: decimal.NewFromInt(1),
140+
MonthlyQuantity: decimalPtr(decimal.NewFromInt(additionalIOPS)),
141+
ProductFilter: &schema.ProductFilter{
142+
VendorName: strPtr("azure"),
143+
Region: strPtr(r.Region),
144+
Service: strPtr("Azure Database for MySQL"),
145+
ProductFamily: strPtr("Databases"),
146+
AttributeFilters: []*schema.AttributeFilter{
147+
{Key: "productName", Value: strPtr("Azure Database for MySQL Flexible Server Storage")},
148+
{Key: "skuName", Value: strPtr("Additional IOPS")},
149+
},
150+
},
151+
}
152+
}
153+
154+
// backupCostComponent returns a cost component for additional backup storage.
155+
func (r *MySQLFlexibleServer) backupCostComponent() *schema.CostComponent {
156+
var quantity *decimal.Decimal
157+
if r.AdditionalBackupStorageGB != nil {
158+
quantity = decimalPtr(decimal.NewFromFloat(*r.AdditionalBackupStorageGB))
159+
}
160+
161+
return &schema.CostComponent{
162+
Name: "Additional backup storage",
163+
Unit: "GB",
164+
UnitMultiplier: decimal.NewFromInt(1),
165+
MonthlyQuantity: quantity,
166+
ProductFilter: &schema.ProductFilter{
167+
VendorName: strPtr("azure"),
168+
Region: strPtr(r.Region),
169+
Service: strPtr("Azure Database for MySQL"),
170+
ProductFamily: strPtr("Databases"),
171+
AttributeFilters: []*schema.AttributeFilter{
172+
{Key: "productName", Value: strPtr("Azure Database for MySQL Flexible Server Backup Storage")},
173+
{Key: "meterName", Value: strPtr("Backup Storage Data Stored")},
174+
},
175+
},
176+
}
177+
}

0 commit comments

Comments
 (0)