Skip to content
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
30 changes: 30 additions & 0 deletions doc/dataSources.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,36 @@ appSync:

- `eventBusArn`: The ARN of the event bus

## Bedrock

```yaml
appSync:
dataSources:
myBedrock:
type: 'AMAZON_BEDROCK_RUNTIME'
config:
models:
- amazon.titan-text-lite-v1
- eu.amazon.nova-2-lite-v1:0
- arn:aws:bedrock:us-east-1:123456789012:inference-profile/us.anthropic.claude-3-5-haiku-20241022-v1:0
```

### config

All fields are optional. When `config` is omitted entirely, the plugin still creates the data source and generates a default service role.

**Prerequisite:** Your AWS account must have access enabled for each Bedrock foundation model you plan to use, in the same region as your AppSync API (and, for cross-region inference profiles, in the regions the profile can route to). Request access in the Amazon Bedrock console before deploying or copying this example; otherwise invocations fail at runtime even when IAM is correct. See [Manage access to Amazon Bedrock foundation models](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) and [Prerequisites for inference profiles](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html) (required for models such as Amazon Nova).

- `models`: Optional list of model identifiers used to scope the generated IAM policy. When omitted, the default role allows `bedrock:InvokeModel` and `bedrock:Converse` on `*`. Each entry is expanded as follows:
- A **bare foundation model ID** (e.g. `amazon.titan-text-lite-v1`) becomes `arn:${AWS::Partition}:bedrock:${region}::foundation-model/<id>`.
- A **cross-region inference profile ID** (prefixed with a geographic code such as `us.`, `eu.`, `apac.`, `us-gov.`, or `global.` — e.g. `eu.amazon.nova-2-lite-v1:0`) is expanded into **two** ARNs: the inference profile in this account/region (`arn:${AWS::Partition}:bedrock:${region}:${AWS::AccountId}:inference-profile/<id>`) and the underlying foundation model across all regions the profile can route to (`arn:${AWS::Partition}:bedrock:*::foundation-model/<base-id>`). Models like Amazon Nova are only invokable through an inference profile, so use the profile ID here (and as the `modelId` in your resolver).
- A **full ARN** (or CloudFormation intrinsic function) is used as-is.
- `region`: AWS region used when expanding model identifiers (foundation-model and inference-profile ARNs). Defaults to the stack region.
- `serviceRoleArn`: The service role ARN for this DataSource. If not provided, a new one will be created.
- `iamRoleStatements`: Statements to use for the generated IAM Role. If not provided, default statements will be used.

Resolvers invoke Bedrock through the `APPSYNC_JS` runtime using `InvokeModel` or `Converse` request objects. AppSync only supports synchronous invocations that complete within 10 seconds; streaming APIs are not supported. See the [AWS AppSync Bedrock resolver reference](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-bedrock-js.html) for request/response shapes and helper utilities.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## NONE

```yaml
Expand Down
73 changes: 73 additions & 0 deletions e2e/datasource-bedrock.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { synthesize } from './helpers/synthesize';
import {
expectDataSourceOfType,
findResourcesByType,
} from './helpers/assertions';

describe('examples/datasource-bedrock', () => {
let result: ReturnType<typeof synthesize>;

beforeAll(() => {
result = synthesize('examples/datasource-bedrock');
});

afterAll(() => {
result.cleanup();
});

it('creates an AMAZON_BEDROCK_RUNTIME data source', () => {
const ds = expectDataSourceOfType(
result.template,
'AMAZON_BEDROCK_RUNTIME',
);
expect(ds.resource.Properties?.Name).toBe('bedrock');
});

it('generates a service role with bedrock invoke permissions for the datasource', () => {
const ds = expectDataSourceOfType(
result.template,
'AMAZON_BEDROCK_RUNTIME',
);
const serviceRoleArn = ds.resource.Properties?.ServiceRoleArn as {
'Fn::GetAtt'?: [string, string];
};
const roleLogicalId = serviceRoleArn?.['Fn::GetAtt']?.[0];
expect(roleLogicalId).toBeDefined();

const roles = findResourcesByType(result.template, 'AWS::IAM::Role');
const bedrockRole = roles.find(
({ logicalId }) => logicalId === roleLogicalId,
);
expect(bedrockRole).toBeDefined();

if (!bedrockRole) {
throw new Error('Expected Bedrock datasource role to be present');
}

const policies = bedrockRole.resource.Properties?.Policies as Array<{
PolicyName?: string;
PolicyDocument?: { Statement?: Array<{ Action?: string | string[] }> };
}>;
expect(
policies?.some(
(policy) => policy.PolicyName === 'AppSync-Datasource-bedrock',
),
).toBe(true);

const actions =
policies?.flatMap(
(policy) =>
policy.PolicyDocument?.Statement?.flatMap((statement) =>
Array.isArray(statement.Action)
? statement.Action
: statement.Action
? [statement.Action]
: [],
) ?? [],
) ?? [];

expect(actions).toEqual(
expect.arrayContaining(['bedrock:InvokeModel', 'bedrock:Converse']),
);
});
});
1 change: 1 addition & 0 deletions e2e/helpers/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export function expectDataSourceOfType(
| 'HTTP'
| 'RELATIONAL_DATABASE'
| 'AMAZON_EVENTBRIDGE'
| 'AMAZON_BEDROCK_RUNTIME'
| 'NONE',
): { logicalId: string; resource: CfnResource } {
return expectResourceWithProperties(template, 'AWS::AppSync::DataSource', {
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ stay current with the plugin's actual behavior — if they break, CI fails.
| [datasource-http](./datasource-http/) | HTTP data source with optional IAM signing |
| [datasource-none](./datasource-none/) | NONE data source (local resolvers) |
| [datasource-eventbridge](./datasource-eventbridge/) | EventBridge data source |
| [datasource-bedrock](./datasource-bedrock/) | Amazon Bedrock runtime data source for synchronous model invocations |
| [datasource-opensearch](./datasource-opensearch/) | Amazon OpenSearch Service data source |
| [datasource-rds](./datasource-rds/) | Relational Database (Aurora Serverless) data source |
| [caching](./caching/) | Server-side caching configuration |
Expand Down
20 changes: 20 additions & 0 deletions examples/datasource-bedrock/resolvers/summarize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { invokeModel } from '@aws-appsync/utils/ai';

export function request(ctx) {
return invokeModel({
modelId: 'eu.amazon.nova-micro-v1:0',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
body: {
schemaVersion: 'messages-v1',
messages: [
{
role: 'user',
content: [{ text: ctx.args.text }],
},
],
},
});
}

export function response(ctx) {
return ctx.result.output.message.content[0].text;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
3 changes: 3 additions & 0 deletions examples/datasource-bedrock/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type Query {
summarize(text: String!): AWSJSON
}
29 changes: 29 additions & 0 deletions examples/datasource-bedrock/serverless.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
service: appsync-datasource-bedrock

provider:
name: aws
runtime: nodejs22.x

plugins:
- serverless-appsync-plugin

appSync:
name: datasource-bedrock
authentication:
type: API_KEY
apiKeys:
- name: default

resolvers:
Query.summarize:
kind: UNIT
dataSource: bedrock
code: ./resolvers/summarize.js

dataSources:
bedrock:
type: AMAZON_BEDROCK_RUNTIME
description: Amazon Bedrock runtime for synchronous model invocations
config:
models:
- eu.amazon.nova-micro-v1:0
Loading
Loading