appsync to appsync integration - http datasource - AWS IAM

Viewed 651

I'm trying to follow best practices and I have two business modules A and B - both with AWS Appsync and IAM.

Now I need to access module B from module A.

So I create HTTP Datasource

(CDK implementation in A module)

        const bModuleApiDS = new HttpDataSource(this, 'BModuleApiDS', {
            api: this.api,
            endpoint: bApiEndpoint,
            name: 'BModuleApiDS',
            description: `Data source for : ${bApiEndpoint}`,
            authorizationConfig: {
                signingRegion: 'eu-west-1',
                signingServiceName: 'appsync',
            },
            serviceRole: iam.Role.fromRoleArn(this, 'ServiceRoleForApiDS', bApiRoleArn),
        } as unknown as HttpDataSourceProps);
  1. bApiRoleArn - is defined in B module and adds access to specific queries (IAM authorization)
  2. bApiEndpoint - appsync url for B module
  3. "as unknown as HttpDataSourceProps" - for some reason HttpDataSourceProps doesn't expose serviceRole (CDK specific), so this is a way how to force it. (maybe because of my cross account issue?)

All works fine in single account (When module A and module B are in the same account). But when A and B are in different accounts, I'm getting the error: Cross-account pass role is not allowed.

Question: How to setup the role / roles / appsync so it's working when module A and module B are in different AWS accounts?


Another try:

  1. Deploy without the role (the default role is created)
  2. Update the role using CLI:
aws appsync update-data-source --api-id kcf4l3xkxxxxxxxcsn6cnfoxm  --name BModuleApiDS--type HTTP --service-role-arn arn:aws:iam::9999999999:role/BModuleGraphqlApi-RoleForAModule

I'm getting following error:

An error occurred (AccessDeniedException) when calling the UpdateDataSource operation: Cross-account pass role is not allowed.
  • where 999999999999 is B module account
  • 111111111111 is A module account
  • arn:aws:iam::111111111111:user/cli is the deployment user with admin access and iam:passRole permission
  • and BModuleGraphqlApi-RoleForAModule is defined as:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "appsync:GraphQL",
            "Resource": "arn:aws:appsync:eu-west-1:999999999999:apis/kcf4l3xkxxxxxxxcsn6cnfoxm/types/Query/fields/queryForModuleA",
            "Effect": "Allow"
        }
    ]
}

with trust relationship:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::111111111111:root",
          "arn:aws:iam::111111111111:user/cli"
        ],
        "Service": "appsync.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

The same question: How to setup the role / roles / appsync so the module A can access the module B in different AWS account using IAM authorization (not api key)?

Wider context how appsync can simplify access to multiple microservices (https://aws.amazon.com/blogs/mobile/appsync-microservices/)

1 Answers

This was asked a while ago and is probably not pertinent anymore, but it might be helpful for others.

As of now (Dec 1, 2021), AppSync does not support IAM authorization mode for cross account AppSync to AppSync communication. I cannot provide the low level reason of why it is not supported.

Fortunately, there is a work around with a small penalty in latency.

| Account A |     | Account B |
| AppSync   | <-> | AppSync   | 

does not work but,

| Account A |     | Account B                    |
| AppSync   | <-> | APIGateway <-> AppSync works |

works well. Another option is to use Lambda

| Account A          |     | Account B |
| AppSync <-> Lambda | <-> | AppSync   |

but to me this approach is too complicated. First, you have to implement the lambda. Second, you have to keep this lambda warm otherwise it will add cold start latencies that can go up to 20 seconds in some cases. So, as a first approach I'll recommend the AppSync <-> APIGateway <-> AppSync.

So in the use case where module A invokes module B, we can still use a HTTP data source in module A but its implementation will be similar to:

    // A role in Module A that AppSync will assume to get permissions
    // to invoke APIGateway endpoint in Module B
    const bModuleDSRole = new Role(this, 'BModuleDSRole', {
      roleName: 'BModuleDSRole',
      assumedBy: new ServicePrincipal('appsync.amazonaws.com')
    });

    new Policy(this, 'BModuleDSPolicy', {
      policyName: 'BModuleDSPolicy',
      roles: [bModuleDSRole],
      statements: [
          new PolicyStatement({
              actions: [
                "execute-api:Invoke"
              ],
              resources: [ <bAPIGwEndpointArn> ]
          })
      ]
    });

    const bModuleDataSource = new CfnDataSource(this, 'BModule', {
      apiId: props.apiId,
      name: 'BModule',
      type: "HTTP",
      httpConfig: {
          endpoint: <bAPIGwEndpoint>,
          authorizationConfig: {
              authorizationType: "AWS_IAM",
              awsIamConfig: {
                  signingRegion: <region>,
                  signingServiceName: "execute-api"
              }
          }
      },
      serviceRoleArn: bModuleDSRole.roleArn
    });

On module B we create an APIGateway endpoint with following elements

import { Construct } from 'monocdk';
import { Stage } from '../config/Stages';
import { AuthorizationType as apigateway_auth_type, AwsIntegration, RestApi,} from 'monocdk/aws-apigateway';
import { GraphqlApi } from 'monocdk/aws-appsync';
import { APIGResourcePolicy } from '@amzn/fin_connect_constructs';
import { Effect, Policy, PolicyStatement, Role, ServicePrincipal } from 'monocdk/aws-iam';

export interface RestApiEndpointProps {
  stage: Stage,
  api: GraphqlApi;
}

export class RestApiEndpoint extends Construct {
  readonly apiId: string;

  constructor(scope: Construct, id: string, props: RestApiEndpointProps) {
    super(scope, id);

    // A role in Module B that APIGateway can assume to invoke AppSync
    // endpoint also from Module B
    // Configure access to MODULEB API
    const moduleBRestApiRole = new Role(this, 'MODULEBRestApiRole', {
      assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
      roleName: 'MODULEBRestApiRole',
    });
  
    new Policy(this, 'MODULEBRestApPolicy', {
      policyName: 'MODULEBRestApPolicy',
      roles: [moduleBRestApiRole],
      statements: [
        new PolicyStatement({
          actions: [
            "appsync:GraphQL"
          ],
          effect: Effect.ALLOW,
          resources: [`${props.api.arn}/types/Query/*`]
        })
      ]
    });

    // A resource policy allow listing the role from Module A.
    // the props.stage.apigResourceAccess is something similar to 
    //apigResourceAccess: [
    //  {
    //    "principle": 'arn:aws:iam::112233445566:role/BModuleDSRole',
    //    "resources": [`execute-api:/prod/*/*`],
    //    "actions": ['execute-api:Invoke']
    //  },
    //],
    const resourcePolicy = new APIGResourcePolicy(this, "APIGResourcePolicy", {
      apigResourceAccess: props.stage.apigResourceAccess
    });

    const api = new RestApi(this, 'moduleb-api-gateway', {
      policy: resourcePolicy.policyDocument
    });

    // This integration will build the AppSync endpoint URL automatically,
    // we just have to provide the subdomain of the endpoint Warning not the 
    // AppSync endpoint API ID but, the subdomain.
    const appsyncIntegration = new AwsIntegration({
      service: 'appsync-api',
      options: {
        credentialsRole: moduleBRestApiRole,
        integrationResponses: [{
          statusCode: '200',
        }],
      },
      path: 'graphql',
      region: props.stage.region,
      subdomain: props.stage.apiSubdomain,
    });

    api.root.addMethod('POST', appsyncIntegration, {
      authorizationType: apigateway_auth_type.IAM,
      methodResponses: [{
        statusCode: '200',
      }],
    });
  }
}

And that is all. APIGateway from module B will receive the requests from AppSync Module A and will forward them to AppSync Module B. On the response will pass back the response to from AppSync Module B to AppSync Module A.

Related