Path: blob/master/src/packages/util/compute/cloud/google-cloud/compute-cost.ts
1451 views
import type { GoogleCloudConfiguration } from "@cocalc/util/db-schema/compute-servers";1import { DNS_COST_PER_HOUR } from "@cocalc/util/compute/dns";23// import debug from "debug";4//const log = debug("cocalc:util:compute-cost");5const log = (..._args) => {};67// copy-pasted from my @cocalc/gcloud-pricing-calculator package to help with sanity in code below.89interface PriceData {10prices?: { [region: string]: number };11spot?: { [region: string]: number };12vcpu?: number;13memory?: number;14count?: number; // for gpu's only15max?: number; // for gpu's only16machineType?: string | { [count: number]: string[] }; // for gpu's only17}1819interface ZoneData {20machineTypes: string; // ['e2','n1','n2', 't2d' ... ] -- array of machine type prefixes21location: string; // description of where it is22lowC02: boolean; // if true, low c02 emissions23gpus: boolean; // if true, has gpus24}2526export interface BucketPricing {27Standard?: number;28Nearline?: number;29Coldline?: number;30Archive?: number;31}3233export type GoogleWorldLocations =34| "APAC"35| "Europe"36| "Middle East"37| "North America"38| "South Africa"39| "South America";4041interface GoogleWorldPrices {42APAC: number;43Europe: number;44"Middle East": number;45"North America": number;46"South Africa": number;47"South America": number;48}4950export interface GoogleCloudData {51machineTypes: { [machineType: string]: PriceData };52disks: {53"pd-standard": { prices: { [zone: string]: number } };54"pd-ssd": { prices: { [zone: string]: number } };55"pd-balanced": { prices: { [zone: string]: number } };56"hyperdisk-balanced-capacity": { prices: { [zone: string]: number } };57"hyperdisk-balanced-iops": { prices: { [zone: string]: number } };58"hyperdisk-balanced-throughput": { prices: { [zone: string]: number } };59};60accelerators: { [acceleratorType: string]: PriceData };61zones: { [zone: string]: ZoneData };62// markup percentage: optionally include markup to always increase price by this amount,63// e.g., if markup is 42, then price will be multiplied by 1.42.64markup?: number;65storage: {66atRest: {67dualRegions: { [region: string]: BucketPricing };68multiRegions: {69asia: BucketPricing;70eu: BucketPricing;71us: BucketPricing;72};73regions: {74[region: string]: BucketPricing;75};76};77dataTransferInsideGoogleCloud: {78APAC: GoogleWorldPrices;79Europe: GoogleWorldPrices;80"Middle East": GoogleWorldPrices;81"North America": GoogleWorldPrices;82"South Africa": GoogleWorldPrices;83"South America": GoogleWorldPrices;84};85dataTransferOutsideGoogleCloud: {86worldwide: number;87china: number;88australia: number;89};90interRegionReplication: {91asia: number;92eu: number;93us: number;94};95retrieval: {96standard: number;97nearline: number;98coldline: number;99archive: number;100};101singleRegionOperations: {102standard: { classA1000: number; classB1000: number };103nearline: { classA1000: number; classB1000: number };104coldline: { classA1000: number; classB1000: number };105archive: { classA1000: number; classB1000: number };106};107};108}109110interface Options {111configuration: GoogleCloudConfiguration;112// output of getData from this package -- https://www.npmjs.com/package/@cocalc/gcloud-pricing-calculator113// except that package is backend only (it caches to disk), so data is obtained via an api, then used here.114priceData: GoogleCloudData;115state?: "running" | "off" | "suspended";116}117118/*119Returns the cost per hour in usd of a given Google Cloud vm configuration,120given the result of getData from @cocalc/gcloud-pricing-calculator.121*/122export default function computeCost({123configuration,124priceData,125state = "running",126}: Options): number {127if (state == "off") {128return computeOffCost({ configuration, priceData });129} else if (state == "suspended") {130return computeSuspendedCost({ configuration, priceData });131} else if (state == "running") {132return computeRunningCost({ configuration, priceData });133} else {134throw Error(`computing cost for state "${state}" not implemented`);135}136}137138function computeRunningCost({ configuration, priceData }) {139const instanceCost = computeInstanceCost({ configuration, priceData });140const diskCost = computeDiskCost({ configuration, priceData });141const externalIpCost = computeExternalIpCost({ configuration, priceData });142const acceleratorCost = computeAcceleratorCost({ configuration, priceData });143const dnsCost = computeDnsCost({ configuration });144log("cost", {145instanceCost,146diskCost,147externalIpCost,148acceleratorCost,149dnsCost,150});151return instanceCost + diskCost + externalIpCost + acceleratorCost + dnsCost;152}153154function computeDnsCost({ configuration }) {155return configuration.dns ? DNS_COST_PER_HOUR : 0;156}157158export function computeInstanceCost({ configuration, priceData }) {159const data = priceData.machineTypes[configuration.machineType];160if (data == null) {161throw Error(162`unable to determine cost since machine type ${configuration.machineType} is unknown. Select a different machine type.`,163);164}165const cost =166data[configuration.spot ? "spot" : "prices"]?.[configuration.region];167if (cost == null) {168if (configuration.spot && Object.keys(data["spot"]).length == 0) {169throw Error(170`spot instance pricing for ${configuration.machineType} is not available`,171);172}173throw Error(174`unable to determine cost since machine type ${configuration.machineType} is not available in the region '${configuration.region}'. Select a different region.`,175);176}177return markup({ cost, priceData });178}179180// Compute the total cost of disk for this configuration, including any markup.181182// for now this is the only thing we support183export const DEFAULT_HYPERDISK_BALANCED_IOPS = 3000;184export const DEFAULT_HYPERDISK_BALANCED_THROUGHPUT = 140;185186export function hyperdiskCostParams({ region, priceData }): {187capacity: number;188iops: number;189throughput: number;190} {191const diskType = "hyperdisk-balanced";192const capacity =193priceData.disks["hyperdisk-balanced-capacity"]?.prices[region];194if (!capacity) {195throw Error(196`Unable to determine ${diskType} capacity pricing in ${region}. Select a different region.`,197);198}199const iops = priceData.disks["hyperdisk-balanced-iops"]?.prices[region];200if (!iops) {201throw Error(202`Unable to determine ${diskType} iops pricing in ${region}. Select a different region.`,203);204}205const throughput =206priceData.disks["hyperdisk-balanced-throughput"]?.prices[region];207if (!throughput) {208throw Error(209`Unable to determine ${diskType} throughput pricing in ${region}. Select a different region.`,210);211}212return { capacity, iops, throughput };213}214215export function computeDiskCost({ configuration, priceData }: Options): number {216const diskType = configuration.diskType ?? "pd-standard";217let cost;218if (diskType == "hyperdisk-balanced") {219// per hour pricing for hyperdisks is NOT "per GB". The pricing is per hour, but the220// formula is not as simple as "per GB", so we compute the cost per hour via221// the more complicated formula here.222const { capacity, iops, throughput } = hyperdiskCostParams({223priceData,224region: configuration.region,225});226cost =227(configuration.diskSizeGb ?? 10) * capacity +228(configuration.hyperdiskBalancedIops ?? DEFAULT_HYPERDISK_BALANCED_IOPS) *229iops +230(configuration.hyperdiskBalancedThroughput ??231DEFAULT_HYPERDISK_BALANCED_THROUGHPUT) *232throughput;233} else {234// per hour pricing for the rest of the disks is just "per GB" via the formula here.235const diskCostPerGB =236priceData.disks[diskType]?.prices[configuration.region];237log("disk cost per GB per hour", { diskCostPerGB });238if (!diskCostPerGB) {239throw Error(240`unable to determine cost since disk cost in region ${configuration.region} is unknown. Select a different region.`,241);242}243cost = diskCostPerGB * (configuration.diskSizeGb ?? 10);244}245return markup({ cost, priceData });246}247248export function computeOffCost({ configuration, priceData }: Options): number {249const diskCost = computeDiskCost({ configuration, priceData });250const dnsCost = computeDnsCost({ configuration });251252return diskCost + dnsCost;253}254255export function computeSuspendedCost({256configuration,257priceData,258}: Options): number {259const diskCost = computeDiskCost({ configuration, priceData });260const memoryCost = computeSuspendedMemoryCost({ configuration, priceData });261const dnsCost = computeDnsCost({ configuration });262263return diskCost + memoryCost + dnsCost;264}265266export function computeSuspendedMemoryCost({ configuration, priceData }) {267// how much memory does it have?268const data = priceData.machineTypes[configuration.machineType];269if (data == null) {270throw Error(271`unable to determine cost since machine type ${configuration.machineType} is unknown. Select a different machine type.`,272);273}274const { memory } = data;275if (!memory) {276throw Error(277`cannot compute suspended cost without knowing memory of machine type '${configuration.machineType}'`,278);279}280// Pricing / GB of RAM / month is here -- https://cloud.google.com/compute/all-pricing#suspended_vm_instances281// It is really weird in the table, e.g., in some places it claims to be basically 0, and in Sao Paulo it is282// 0.25/GB/month, which seems to be the highest. Until I nail this down properly with SKU's, for cocalc283// we will just use 0.25 + the markup.284const cost = (memory * 0.25) / 730;285return markup({ cost, priceData });286}287288// TODO: This could change and should be in pricing data --289// https://cloud.google.com/vpc/network-pricing#ipaddress290export const EXTERNAL_IP_COST = {291standard: 0.005,292spot: 0.0025,293};294295export function computeExternalIpCost({ configuration, priceData }) {296if (!configuration.externalIp) {297return 0;298}299let cost;300if (configuration.spot) {301cost = EXTERNAL_IP_COST.spot;302} else {303cost = EXTERNAL_IP_COST.standard;304}305return markup({ cost, priceData });306}307308export function computeAcceleratorCost({ configuration, priceData }) {309if (!configuration.acceleratorType) {310return 0;311}312// we have 1 or more GPUs:313const acceleratorCount = configuration.acceleratorCount ?? 1;314// sometimes google has "tesla-" in the name, sometimes they don't,315// but our pricing data doesn't.316const acceleratorData =317priceData.accelerators[configuration.acceleratorType] ??318priceData.accelerators[configuration.acceleratorType.replace("tesla-", "")];319if (acceleratorData == null) {320throw Error(`unknown GPU accelerator ${configuration.acceleratorType}`);321}322323if (324typeof acceleratorData.machineType == "string" &&325!configuration.machineType.startsWith(acceleratorData.machineType)326) {327throw Error(328`machine type for ${configuration.acceleratorType} must be ${acceleratorData.machineType}. Change the machine type.`,329);330}331if (typeof acceleratorData.machineType == "object") {332let v: string[] = acceleratorData.machineType[acceleratorCount];333if (v == null) {334throw Error(`invalid number of GPUs`);335}336if (!v.includes(configuration.machineType)) {337throw Error(338`machine type for ${339configuration.acceleratorType340} with count ${acceleratorCount} must be one of ${v.join(", ")}`,341);342}343}344let costPer =345acceleratorData[configuration.spot ? "spot" : "prices"]?.[346configuration.zone347];348log("accelerator cost per", { costPer });349if (costPer == null) {350throw Error(351`GPU accelerator ${configuration.acceleratorType} not available in zone ${configuration.zone}. Select a different zone.`,352);353}354return markup({ cost: costPer * acceleratorCount, priceData });355}356357export const DATA_TRANSFER_OUT_COST_PER_GiB = 0.15;358export function computeNetworkCost(dataTransferOutGiB: number): number {359// The worst possible case is 0.15360// https://cloud.google.com/vpc/network-pricing361// We might come up with a most sophisticated and affordable model if we362// can figure it out; however, it seems possibly extremely difficult.363// For now our solution will be to charge a flat 0.15 fee, and don't364// include any markup.365const cost = dataTransferOutGiB * DATA_TRANSFER_OUT_COST_PER_GiB;366return cost;367}368369export function markup({ cost, priceData }) {370if (priceData.markup) {371return cost * (1 + priceData.markup / 100.0);372}373return cost;374}375376377