😀
AWS料金をSlackに通知するやつ
前書き
よりAWSのコストを監視しやすくするために、
月初からの請求額を毎日Slackに通知するシステムをサーバーレスで構築してみました。
完成品は下記の図の通りです。
実装
最初はGo 1.xのランタイムを使用していましたが、そのランタイムがAWSで廃止されたため、Node.jsのランタイムを使用してコードを書き直しました。
プロジェクト初期化、serverless frameworkを使用してます、初期化は公式のv3テンプレート使用します、その方が生成されたファイル構成がシンプルです。
serverless create --template-url https://github.com/serverless/examples/tree/v3/aws-node-typescript --path aws-nodejs-typescript-2
AWSのコスト取得するためにaws-sdk/client-cost-explorerを使用します。
yarn add @aws-sdk/client-cost-explorer
serverless.yml
の設定、今回やることはシンプルです、lambda関数からcost-explorer
にアクセスできれば良いので、下記のiamを追加します。
serverless.yml
resources:
Resources:
+ BillingIamRole:
+ Type: AWS::IAM::Role
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: lambda.amazonaws.com
+ Action: "sts:AssumeRole"
+ Policies:
+ - PolicyName: CostExplorerPolicy
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Action:
+ - "ce:GetCostAndUsage"
+ Resource: "*"
provider:
name: aws
runtime: nodejs18.x
stage: ${opt:stage, 'dev'}
region: ap-northeast-1
profile: default
iam:
+ role: BillingIamRole
...その他省略
コードの実装はこちら
handeler.ts
import * as AWS from '@aws-sdk/client-cost-explorer';
const SLACK_WEBHOOK_URL: string | undefined = process.env.SLACK_WEBHOOK_URL;
const AWS_REGION: string | undefined = process.env.AWS_REGION;
const costExplorer = new AWS.CostExplorer({ region: AWS_REGION });
interface TotalBilling {
start: string;
end: string;
billing: number;
}
interface ServiceCost {
serviceName: string;
cost: number;
}
async function getTotalBilling(): Promise<TotalBilling> {
const [startDate, endDate] = getMonthlyCostDateRange()
const params = {
TimePeriod: {
Start: startDate,
End: endDate,
},
Granularity: AWS.Granularity.MONTHLY,
Metrics: ['AmortizedCost'],
};
const result = await costExplorer.getCostAndUsage(params);
const billingAmount = parseFloat(result.ResultsByTime![0].Total!['AmortizedCost'].Amount!);
return {
start: startDate,
end: endDate,
billing: billingAmount,
};
}
async function getServiceCosts(): Promise<ServiceCost[]> {
const [startDate, endDate] = getMonthlyCostDateRange()
const params = {
TimePeriod: {
Start: startDate,
End: endDate,
},
Granularity: AWS.Granularity.MONTHLY,
Metrics: ['AmortizedCost'],
GroupBy: [
{
Type: AWS.GroupDefinitionType.DIMENSION,
Key: AWS.Dimension.SERVICE,
},
],
};
const result = await costExplorer.getCostAndUsage(params);
const serviceCosts: ServiceCost[] = result.ResultsByTime![0].Groups!.map((group) => {
return {
serviceName: group.Keys![0],
cost: parseFloat(group.Metrics!['AmortizedCost'].Amount!),
};
});
return serviceCosts;
}
function formatServiceCosts(totalBilling: TotalBilling, serviceCosts: ServiceCost[]): string {
let formattedText = `期間:${totalBilling.start} 〜 ${totalBilling.end}\n合計金額:$${totalBilling.billing.toFixed(2)}`;
formattedText += '\n\n各サービスごとの料金:\n';
serviceCosts.forEach((serviceCost) => {
if (serviceCost.cost.toFixed(2) === "0.00") {
return;
}
formattedText += `${serviceCost.serviceName}: $${serviceCost.cost.toFixed(2)}\n`;
});
return formattedText;
}
export async function hello(event: any, context: Context): Promise<void> {
try {
const totalBilling = await getTotalBilling();
const serviceCosts = await getServiceCosts();
const payload = {
attachments: [
{
color: '#36a64f',
pretext: '今月のAWS利用費用の合計金額',
text: formatServiceCosts(totalBilling, serviceCosts),
},
],
};
const payloadBytes = Buffer.from(JSON.stringify(payload));
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: payloadBytes,
};
const response = await fetch(SLACK_WEBHOOK_URL!, requestOptions);
if (response.status !== 200) {
console.error(`Slack notification failed with status: ${response.status}`);
}
} catch (error) {
console.error('Error:', error);
}
}
function getMonthlyCostDateRange(): [string, string] {
const currentDate = new Date();
const startOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
if (currentDate.getDate() === 1) {
const lastMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1);
return [lastMonth.toISOString().slice(0, 10), currentDate.toISOString().slice(0, 10)];
}
return [startOfMonth.toISOString().slice(0, 10), currentDate.toISOString().slice(0, 10)];
}
SLACK_WEBHOOK_URLはIncoming Webhookから取得してください。
おまけ
SSOログイン使ってlambdaデプロイもできるようです。
serverless.yml
があるディレクトリでインストールします。
npm install --save-dev serverless-better-credentials
pluginsに追加
serverless.yml
+ plugins:
+ - serverless-better-credentials
デプロイする際にssoできるプロフィール指定すれば良いです。
serverless deploy --aws-profile sso-profile
参考記事
Discussion