Skip to content

Add tag for cost management #1167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/en/DEPLOY_OPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,33 @@ EventBridge rules are used for scheduling, and Step Functions for process contro
> - Currently, there's no feature to notify of startup/shutdown errors.
> - Each time the index is recreated, the IndexId and DataSourceId change. If other services reference these, you'll need to adapt to these changes.

### How to Set Tags

GenU supports tags for cost management and other purposes. The key name of the tag is automatically set to `GenU` `. Here are examples of how to set them:

Setting in `cdk.json`:

```json
// cdk.json
...
"context": {
"tagValue": "dev",
...
```

Setting in `parameter.ts`:

```typescript
...
tagValue: "dev",
...
```

However, tags cannot be used with some resources:

- Cross-region inference model calls
- Voice chat model calls

## Enabling Monitoring Dashboard

Create a dashboard that aggregates input/output token counts and recent prompts.
Expand Down
28 changes: 28 additions & 0 deletions docs/ja/DEPLOY_OPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,34 @@ Kendraのインデックスが削除されても、RAG機能はオンのまま
> - 現状では、起動・停止のエラーを通知する機能はありません。
> - インデックスを再作成するたびに、IndexIdやDataSourceIdが変わります。他のサービスなどから参照している場合は、その変更に対応する必要があります。

### タグを設定する方法

GenU ではコスト管理等に使うためのタグをサポートしています。タグのキー名には、自動で `GenU` `が設定されます。
以下に設定例を示します。

`cdk.json` での設定方法

```json
// cdk.json
...
"context": {
"tagValue": "dev",
...
```

`parameter.ts` での設定方法

```typescript
...
tagValue: "dev",
...
```

ただし、いくつかのリソースについてタグが利用できません。

- クロスリージョン推論のモデル呼び出し
- 音声チャットのモデル呼び出し

## モニタリング用のダッシュボードの有効化

入力/出力 Token 数や直近のプロンプト集などが集約されたダッシュボードを作成します。
Expand Down
7 changes: 7 additions & 0 deletions packages/cdk/bin/generative-ai-use-cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { getParams } from '../parameter';
import { createStacks } from '../lib/create-stacks';
import { TAG_KEY } from '../consts';

const app = new cdk.App();
const params = getParams(app);
if (params.tagValue) {
cdk.Tags.of(app).add(TAG_KEY, params.tagValue, {
// Exclude OpenSearchServerless Collection from tagging
excludeResourceTypes: ['AWS::OpenSearchServerless::Collection'],
});
}
createStacks(app, params);
1 change: 1 addition & 0 deletions packages/cdk/cdk.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"context": {
"env": "",
"tagValue": null,
"ragEnabled": false,
"kendraIndexArn": null,
"kendraIndexLanguage": "ja",
Expand Down
2 changes: 2 additions & 0 deletions packages/cdk/consts.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as lambda from 'aws-cdk-lib/aws-lambda';

export const LAMBDA_RUNTIME_NODEJS = lambda.Runtime.NODEJS_22_X;

export const TAG_KEY = 'GenU';
146 changes: 146 additions & 0 deletions packages/cdk/custom-resources/apply-tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
const {
OpenSearchServerlessClient,
TagResourceCommand,
UntagResourceCommand,
ListTagsForResourceCommand,
} = require('@aws-sdk/client-opensearchserverless');

exports.handler = async (event, context) => {
console.log('Event:', JSON.stringify(event, null, 2));

try {
const { collectionId, region, accountId, tag } = event.ResourceProperties;

// Skip for Delete operation
if (event.RequestType === 'Delete') {
return await sendResponse(
event,
context,
'SUCCESS',
{},
'ApplyTagsResource'
);
}

// Create OpenSearch Serverless client
const ossClient = new OpenSearchServerlessClient({ region });
const collectionArn = `arn:aws:aoss:${region}:${accountId}:collection/${collectionId}`;

// Check if we need to apply or remove tags
if (tag && tag.value) {
console.log(
`Applying tags to collection ${collectionId}: ${JSON.stringify(tag)}`
);

// Apply tags
const command = new TagResourceCommand({
resourceArn: collectionArn,
tags: [tag],
});

const res = await ossClient.send(command);

console.log(`response: ${JSON.stringify(res)}`);
console.log(`Successfully applied tags to ${collectionArn}`);
} else {
// If tagValue is unset, we need to check if the tag exists and remove it
console.log(
`Checking for existing tags on collection ${collectionId} with key ${tag.key}`
);

// First, list existing tags
const listTagsCommand = new ListTagsForResourceCommand({
resourceArn: collectionArn,
});

const existingTags = await ossClient.send(listTagsCommand);
console.log(`Existing tags: ${JSON.stringify(existingTags)}`);

// Check if our tag key exists
const tagExists =
existingTags.tags && existingTags.tags.some((t) => t.key === tag.key);

if (tagExists) {
console.log(
`Removing tag with key ${tag.key} from collection ${collectionId}`
);

// Remove the tag
const untagCommand = new UntagResourceCommand({
resourceArn: collectionArn,
tagKeys: [tag.key],
});

const untagRes = await ossClient.send(untagCommand);
console.log(`Untag response: ${JSON.stringify(untagRes)}`);
console.log(
`Successfully removed tag with key ${tag.key} from ${collectionArn}`
);
} else {
console.log(
`No tag with key ${tag.key} found on collection ${collectionId}`
);
}
}

return await sendResponse(
event,
context,
'SUCCESS',
{},
'ApplyTagsResource'
);
} catch (error) {
console.error('Error:', error);
return await sendResponse(
event,
context,
'FAILED',
{},
'ApplyTagsResource'
);
}
};

// Function to send response to CloudFormation
async function sendResponse(event, context, status, data, physicalId) {
const responseBody = JSON.stringify({
Status: status,
Reason: `See CloudWatch Log Stream: ${context.logStreamName}`,
PhysicalResourceId: physicalId || context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: data,
});

return await new Promise((resolve, reject) => {
const https = require('https');
const url = require('url');
const parsedUrl = url.parse(event.ResponseURL);

const options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.path,
method: 'PUT',
headers: {
'Content-Type': '',
'Content-Length': responseBody.length,
},
};

const request = https.request(options, (response) => {
console.log(`Status code: ${response.statusCode}`);
resolve();
});

request.on('error', (error) => {
console.log('send() error:', error);
resolve(); // Still resolve to avoid CF waiting
});

request.write(responseBody);
request.end();
});
}
Loading