KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

クロスアカウントアクセスでAppSync Private APIを使う話

はじめに

こんにちは、LINE上で動くおくすり連絡帳 Pocket Musubi というサービスを開発している種岡です . 社内でプライベートAPIを開発する要件があり、タイミング良くAppSyncでプライベートAPIが使えるようになったため試してみました

全体像

以下システムの全体像になります . アカウントAのLambdaからアカウントBのAppSyncにリクエストし、DynamoDBにItemが書き込まれることをゴールとしました

ポイント

  • AppSyncはPrivate APIモードかつ、認証モードはIAM認証を利用
  • アカウントAとアカウントBはVPC Peeringで繋いでいるためCIDR情報が重複しないようにする
  • クロスアカウントアクセスのためアカウントAとBそれぞれに、適切なロールとポリシーを設定

1. アカウントAでVPCとLambdaを作成

以下CDKのコードになります . 概要としては、VPC、サブネット、セキュリティグループ、Lambdaを作成しています . Labmdaにはプライベートのサブネットをアタッチしています

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';

export class TestAccountAStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'VPCAccountA', {
      cidr: '192.168.1.0/24',
      subnetConfiguration: [
        {
          name: 'PublicSubnet',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          name: 'PrivateSubnet',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
    });

    const securityGroup = new ec2.SecurityGroup(this, 'VPCAccountASecurityGroup', {
      vpc,
      securityGroupName: 'AccountASecurityGroup',
    });

    const lambdaFunction = new lambda.NodejsFunction(this, 'LambdaFunction', {
      entry: 'lambda/index.ts',
      handler: 'handler',
      vpc,
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
      }),
      securityGroups: [securityGroup],
    });
  }
}

2. アカウントBでAppSync Private APIを作成する

作成方法は、AWSブログを参考にしています

  1. PrivateAPIをON

  2. AppSyncのリゾルバとしてDynamoDBを使うための設定

  3. AppSyncが作成されたら、認証モードをIAM認証に変更 後ほど利用するので、青枠のエンドポイントを控えておきます

3. アカウントBでVPCの作成

AppSync用のインターフェースエンドポイントを作成する前に、VPCを作成します . VPC ConsoleのVPCウィザードを使って以下のようなVPCを作成しました

AWSのドキュメントにもさらっと記載されていますが、「DNS 解決を有効化」と「DNS ホスト名を有効化」のオプションにチェックを入れて作成しました

4. アカウントB側でAppSync用のインターフェースエンドポイントを作成

  1. インターフェースエンドポイントに紐付けるセキュリティグループを新規作成 インバウンドルールにHTTPSにアカウントAのVPCのレンジからの許可の設定をする

  2. エンドポイント作成画面

    • サービスでappsyncを選択
    • VPCは先程作成したものを選択

    • サブネットはPrivateのものを選択

    • セキュリティグループは先程作成したものを選択し、ポリシーは動作確認の都合上フルアクセスで作成

      • 利用状況に応じて制限を加えることがドキュメントで推奨されているので、以下のような制限を追加しておくと良さそうです ``` { Statement": [ { "Action": "appsync:*", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::{アカウントB}:role/iamrole-account-b-appsync-private-api" }, "Resource": "arn:aws:appsync:ap-northeast-1:{アカウントB}:apis/rfphskqs2rcbroukizyggtfatm" # AppSyncのARN } } }
    • エンドポイント生成後にできるインターフェース VPC エンドポイントパブリック DNS 名を控えておく

5. VPC Peering Connectionの作成

アカウントAとアカウントBのVPCをつなげる設定をします

今回はアカウントAで作成を行っているので、アカウントB側で承認処理を行う

承認処理後にアカウントAとBそれぞれのVPCのPrivateサブネットのルートテーブルにルートを追加(以下アカウントB側での例)

6. アカウントAのLambdaがアカウントBのAppSync実行するための設定

こちらを参考にアカウントB側でアカウントAのLambdaがAppSyncを呼び出せるためのロールを作成します

  1. 事前に、アカウントAのLambdaにアタッチしているロールのARNを控えておく
  2. アカウントB側で以下のような信頼関係のポリシーを設定したIAMロールを作成します

  3. アカウントAのLambdaに上記で作成したポリシーをアタッチ ```ts import * as cdk from 'aws-cdk-lib'; import * as iam from 'aws-cdk-lib/aws-iam'; // 追加 import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs'; import { Construct } from 'constructs';

    export class TestAccountAStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props);

     const vpc = new ec2.Vpc(this, 'VPCAccountA', {
       cidr: '192.168.1.0/24',
       subnetConfiguration: [
         {
           name: 'PublicSubnet',
           subnetType: ec2.SubnetType.PUBLIC,
         },
         {
           name: 'PrivateSubnet',
           subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
         },
       ],
     });
    
     const securityGroup = new ec2.SecurityGroup(this, 'VPCAccountASecurityGroup', {
       vpc,
       securityGroupName: 'AccountASecurityGroup',
     });
    
     const lambdaFunction = new lambda.NodejsFunction(this, 'LambdaFunction', {
       entry: 'lambda/index.ts',
       handler: 'handler',
       vpc,
       vpcSubnets: vpc.selectSubnets({
         subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
       }),
       securityGroups: [securityGroup],
     });
    
     // 以下追加
     const policy = new iam.PolicyStatement({
       effect: iam.Effect.ALLOW,
       actions: ['sts:AssumeRole'],
       resources: ['arn:aws:iam::{アカウントB}:role/iamrole-account-b-appsync-private-api'],
     });
    
     lambdaFunction.addToRolePolicy(policy);
    

    } } ```

7. LambdaAからアカウントBのAppSyncにMutationクエリを発行して動作確認

以下動作検証用に作成したLambdaのコードになります

import * as AWS from 'aws-sdk';

exports.handler = async (event: any) => {
  // アカウントBから一時的な認証情報を取得
  const stsConnection = new AWS.STS();
  const accountB = await stsConnection
    .assumeRole({
      RoleArn: 'arn:aws:iam::{アカウントB}:role/iamrole-account-b-appsync-private-api',
      RoleSessionName: 'cross_acct_lambda'
    })
    .promise();

  const ACCESS_KEY = accountB.Credentials!.AccessKeyId;
  const SECRET_KEY = accountB.Credentials!.SecretAccessKey;
  const SESSION_TOKEN = accountB.Credentials!.SessionToken;

  // インターフェースエンドポイント作成時に生成された VPCエンドポイントパブリックDNS名を利用
  const uri = 'https://vpce-05ef12cdfccf62bbf-8o6p5qqe.appsync-api.ap-northeast-1.vpce.amazonaws.com/graphql';
  // AppSyncのホスト名
  const hostName = 'r7fgojojmfbabbbw2nuvytspli.appsync-api.ap-northeast-1.amazonaws.com';

  const credentials = new AWS.Credentials({
    accessKeyId: ACCESS_KEY,
    secretAccessKey: SECRET_KEY,
  });
  credentials.sessionToken = SESSION_TOKEN;

  const bodyItem = {
    query: `
      mutation createTodo($CreateTodoInput: CreateTodoInput!) {
        createTodo(input: $CreateTodoInput) {
          id
          title
          description
          priority
        }
      }
    `,
    operationName: 'createTodo',
    variables: {
      CreateTodoInput: {
        title: 'Hello, world!',
        description: 'hoge',
        priority: 7,
      },
    },
  };

  // 署名付きAWS APIリクエストを作成
  // ref. https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/create-signed-request.html
  const apiUrl = new URL(uri);
  const _signatureV4 = new SignatureV4({
    service: 'appsync',
    region: 'ap-northeast-1',
    credentials: credentials,
    sha256: Sha256,
  });
  const httpRequest = new HttpRequest({
    headers: {
      'content-type': 'application/json',
      host: hostName,
    },
    body: JSON.stringify(bodyItem),
    hostname: apiUrl.hostname,
    method: 'POST',
    path: apiUrl.pathname,
  });
  const { headers, body, method }  = await _signatureV4.sign(httpRequest);
  const res = await fetch(uri, {
    headers,
    body,
    method,
  }).then((_res) => _res.json());
  return;
}

おわりに

AppSyncのPrivate APIを作成し、クロスアカウントアクセスでクエリをリクエストすることができました . プライベートAPI作成時に、API Gatewayだけでなく、AppSyncも選択肢となり得ることでよりよい設計ができそうです