Amazon Web Services 한국 블로그

AWS AppSync에서 다양한 AWS 서비스 직접 호출하기

AWS AppSync는 관리형 GraphQL 서비스로서 개발자가 손쉽게 데이터 기반의 모바일 및 웹 애플리케이션을 구축할 수 있게 해 줍니다. 개발자는 서버리스 백엔드를 사용하여 AppSync를 Amazon DynamoDB, AWS LambdaAmazon Elasticsearch Service를 포함한 다양한 데이터 원본에 연결하여 GraphQL API를 구축할 수 있습니다. AWS AppSync는 2018년 5월에 HTTP 데이터 원본에 대한 지원을 추가하여 레거시 API를 GraphQL 엔드포인트에 추가하는 것이 쉬워졌습니다.

AppSync는 HTTP 데이터 원본을 통한 AWS 서비스 호출을 지원하도록 기능이 확장되었습니다. AWS에서 HTTP 요청을 식별하고 승인하려면 해당 요청이 Signature Version 4 프로세스로 서명되어야 합니다. 그렇지 않은 경우 이러한 요청은 거부됩니다. AWS AppSync는 이제 HTTP 데이터 원본 구성의 일부로 제공된 IAM 역할을 기준으로 사용자를 대신해 서명을 계산할 수 있습니다.

이는 중간 단계의 Lambda 함수를 작성하지 않아도 광범위한 AWS 서비스를 호출할 수 있음을 의미합니다. 예를 들어, AWS Step Functions 상태 시스템의 실행을 시작하거나, AWS Secrets Manager에서 보안 암호를 검색하거나, AWS AppSync 리졸버에서 AWS AppSync 자체로부터 사용 가능한 GraphQL API의 목록을 검색할 수 있습니다.

AWS AppSync 긴 실행 요청 구현

새로운 기능의 유용성을 시연하기 위해 긴 실행을 요청하는 GraphQL API를 구축해 보겠습니다. AWS AppSync는 GraphQL 쿼리 실행을 최대 30초로 제한하고 있습니다. 그러나 최대 1분이 소요되는 쿼리(예: 검색)가 있을 수 있습니다. 여기에서는 AWS Step Functions를 사용하여 여러 단계에 걸쳐 장기 실행되는 쿼리를 조정하겠지만 다른 방식으로도 이 기능을 구현할 수 있습니다. AWS AppSync 구독을 사용하여 쿼리가 종료될 때 비동기적으로 클라이언트를 업데이트해 보겠습니다.

HTTP 데이터 원본 생성

AWS AppSync에서 검색 상태 시스템의 실행을 시작하려면 먼저 새 HTTP 데이터 원본을 정의합니다. AWS 서비스를 호출할 두 개의 추가 구성 요소를 제공해야 합니다.

  • AWS AppSync는 Step Functions 상태 시스템을 위한 states:StartExecution을 호출할 권한을 가진 IAM 역할을 맡을 수 있습니다.
  • 서명 구성

AWS AppSync는 현재 AWS CLI, SDK 및 AWS CloudFormation을 사용하여 이러한 구성 요소를 추가하는 기능에 대한 지원을 제공하고 있습니다.

먼저, 다음과 같은 정책을 포함하는 새로운 IAM 역할을 생성합니다(역할의 ARN(Amazon Resource Name)을 기록해 두어야 합니다):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "states:StartExecution"
            ],
            "Resource": [
                "arn:aws:states:<REGION>:<ACCOUNT_ID>:stateMachine:aws-appsync-long-query"
            ],
            "Effect": "Allow"
        }
    ]
}

And trust relationship:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "appsync.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

다음으로 AWS AppSync를 위한 HTTP 데이터 원본을 생성합니다. 이렇게 하려면 http.json이라는 이름을 가진 다음과 같은 구성 파일을 생성합니다.

{
    "endpoint": "https://states.<REGION>.amazonaws.com/",
    "authorizationConfig": {
        "authorizationType": "AWS_IAM",
        "awsIamConfig": {
            "signingRegion": "<REGION>",
            "signingServiceName": "states" 
        }
    }
}

그런 다음 AWS CLI를 사용하여 데이터 원본을 생성합니다.

$ aws appsync create-data-source --api-id <API_ID> \
                                 --name StepFunctionHttpDataSource \
                                 --type HTTP \
                                 --http-config file:///http.json \
                                 --service-role-arn <ROLE_ARN>

또는, AWS CloudFormation을 사용하여 동일한 데이터 원본을 생성할 수 있습니다.

StepFunctionsHttpDataSource:
    Type: AWS::AppSync::DataSource
    Properties:
      ApiId: !GetAtt SearchApi.ApiId
      Name: StepFunctionsHttpDataSource
      Description: Step Functions HTTP
      Type: HTTP
      # IAM role defined elsewhere in AWS CloudFormation template
      ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn
      HttpConfig:
        Endpoint: !Sub https://states.${AWS::Region}.amazonaws.com/
        AuthorizationConfig:
          AuthorizationType: AWS_IAM
          AwsIamConfig:
            SigningRegion: !Ref AWS::Region
            SigningServiceName: states

HTTP 데이터 원본이 정의되었고 Step Functions에 대한 요청을 서명하도록 구성되었으면 GraphQL API를 구축할 수 있습니다.

GraphQL API 구축

장기 실행되는 쿼리를 활성화하려면 검색 요청을 제출하고 즉시 검색 상태([PENDING])와 쿼리에 대한 고유 식별자를 반환합니다. 그런 다음 클라이언트는 해당 특정 쿼리에 대한 업데이트에 구독해야 합니다. GraphQL 스키마는 다음과 같습니다.

type Result {
  id: ID!
  status: ResultStatus!
  listings: [String]
}

enum ResultStatus {
  PENDING
  COMPLETE
  ERROR
}

input ResultInput {
  id: ID!
  status: ResultStatus!
  listings: [String]!
}

type Query {
  # 장기 실행되는 검색을 초기화하도록 클라이언트가 호출
  search(text: String!): Result
}

type Mutation {
  # 검색이 완료되었을 때 백엔드에서 호출
  publishResult(result: ResultInput): Result
}

type Subscription {
  onSearchResult(id: ID!): [Result]
    @aws_subscribe(mutations: [ "publishResult" ])
}

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

다음에는 AWS Management Console, AWS CLI, SDK 또는 AWS CloudFormation의 검색 쿼리를 위한 리졸버를 정의합니다. 이전에 생성한 HTTP 데이터 원본을 선택하고 다음과 같이 요청 매핑 템플릿을 구성합니다.

$util.qr($ctx.stash.put("executionId", $util.autoId()))

{
  "version": "2018-05-29",
  "method": "POST",
  "resourcePath": "/",
  "params": {
    "headers": {
      "content-type": "application/x-amz-json-1.0",
      "x-amz-target":"AWSStepFunctions.StartExecution"
    },
    "body": {
      "stateMachineArn": "arn:aws:states:<REGION>:<ACCOUNT_ID>:stateMachine:aws-appsync-long-query",
      "input": "{ \"name\": \"$ctx.stash.executionId\" }"
    }
  }
}

이 매핑에는 몇 가지 참고할 사항이 있습니다.

  • 여기에서는 AWS AppSync $util.autoId() 함수를 사용하여 검색 쿼리에 대한 고유 식별자를 생성합니다. 이 값은 컨텍스트 스태시에 저장되었다가 나중에 클라이언트에 반환됩니다. 또한 이 값은 상태 시스템에 입력 파라미터로 전달됩니다.
  • x-amz-target 머리글은 호출할 AWS 서비스 및 관련 작업을 지정합니다.
  • 상태 시스템에 대한 입력과 같은 JSON 입력 데이터는 이중 이스케이프되어야 합니다.

이 리졸버에 대한 응답 매핑은 즉각적으로 쿼리 식별자와 PENDING 상태를 반환합니다.

{
  "id": "${ctx.stash.executionId}",
  "status": "PENDING"
}

이제 검색 쿼리를 실행하면 AWS AppSync에서 원하는 Step Functions 상태 시스템의 실행을 시작하고 실행 식별자를 파라미터로 전달합니다. 상태 시스템의 마지막 단계는 Lambda 함수를 호출하는 작업 단계입니다. 이 함수는 AppSync 전이를 트리거합니다. 이는 구독을 통해 클라이언트에 결과를 게시되는 작업을 유발합니다.

장기 실행되는 쿼리 구현 및 결과 반환

이제 AWS AppSync 쿼리가 Step Functions 상태 시스템의 실행을 시작했으므로 쿼리를 구현하고 응답을 클라이언트에 반환할 수 있습니다. 여기에서는 시연을 위해 매우 간단한 구조의 상태 시스템이 사용되었습니다. 다음 다이어그램과 같이 시스템은 1분을 기다린 후 “Return Result” 작업을 호출합니다.

이 간단한 상태 시스템 정의를 쉽게 업데이트하여 더욱 복잡하거나 다단계로 구성된 쿼리를 지원할 수 있습니다. 단 정의는 AWS AppSync 전이를 통해 결과를 반환해야 합니다.

일반적으로 GraphQL 클라이언트는 데이터 원본의 변경을 유발하는 데이터 전이를 실행하며 이는 구독자에게 전송되는 변경 알림을 트리거합니다. 여기에서는 결과에 대한 알림을 트리거하기 위해 전이를 호출하는 Lambda 함수를 정의합니다.

GraphQL 라이브러리를 포함하지 않고 대신 AWS AppSync 엔드포인트에 대한 간단한 HTTP POST를 사용하여 전이를 위한 적절한 페이로드를 전달하겠습니다. 여기에 설명된 샘플은 AWS AppSync API_KEY 승인 유형을 사용하지만 프로덕션 시나리오에서는 AWS_IAM이 권장되며 요청의 서명도 필요할 것입니다. 이 간단한 Return Result 함수는 nodejs8.10 런타임을 사용하여 다음과 같이 구현됩니다.

const PublishResultMutation = `mutation PublishResult(
    $id: ID!,
    $status: ResultStatus!,
    $listings: [String]!
  ) {
    publishResult(result: {id: $id, status: $status, listings: $listings}) {
      id
      status
      listings
    }
  }`;

const executeMutation = async(id) => {
  const mutation = {
    query: PublishResultMutation,
    operationName: 'PublishResult',
    variables: {
      id: id,
      status: 'COMPLETE',
      listings: [ "foo", "bar" ]
    },
  };

  try {
    let response = await axios({
      method: 'POST',
      url: process.env.APPSYNC_ENDPOINT,
      data: JSON.stringify(mutation),
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': process.env.APPSYNC_API_KEY,
      }
    });
    console.log(response.data);
  } catch (error) {
    console.error(`[ERROR] ${error.response.status} - ${error.response.data}`);
    throw error;
  }
};

exports.handler = async(event) => {
  // 쿼리의 고유 식별자를 event.name으로 전달
  await executeMutation(event.name)
  return { message: `finished` }
}

AWS AppSync는 이 특정 쿼리 결과를 구독하는 클라이언트에 알림을 전송합니다. 그러면 클라이언트가 적절히 업데이트할 수 있습니다.

클라이언트 구현

AWS Amplify를 사용하면 쿼리를 제출하고 반환된 식별자를 사용하여 결과를 구독하는 과정을 시연하는 간단한 클라이언트를 구현할 수 있습니다.

import Amplify, { API, graphqlOperation } from "aws-amplify";

// 1. 검색 쿼리 제출
const { data: { search } } = await API.graphql(
    graphqlOperation(searchQuery, { text: 'test' })
);
console.log(`Query ID: ${search.id}`);

// 2. 검색 결과 구독
const subscription = API.graphql(
        graphqlOperation(onSearchResultSubscription, { queryId: search.id })
    ).subscribe({
        next: (result) => {
            // 구독으로부터 데이터 업데이트 수신 중지
            subscription.unsubscribe();
            console.log(result);
        }
    });

이 프로젝트의 실제 코드는 GitHub에서 찾을 수 있습니다.

AWS AppSync에서 직접 AWS 서비스를 호출하면 장기 실행되는 쿼리의 구현 또는 AWS AppSync를 다른 서비스를 위한 GraphQL 전면으로 사용하는 기능의 활성화 등의 몇 가지 사용 사례를 간소화할 수 있습니다. 자세한 내용은 AWS AppSync 개발자 안내서를 참조하십시오. AWS에서는 이 새 기능을 여러분이 어떻게 활용하는지에 대해 많은 관심을 가지고 있습니다. 피드백이 있으시면 AWS AppSync 포럼을 통해 알려주시기 바랍니다.

– Josh Kahn, AWS 솔루션스 아키텍트