워크플로 재시도의 비즈니스 효과 반복 방지

AWSBeginner
지금 연습하기

소개

작업자가 주문을 저장하지만 결과가 워크플로에 도달하기 전에 실패합니다. 재시도나 반복 실행이 원래 주문을 유지하도록 쓰기를 보호하며 다른 주문은 여전히 성공할 수 있도록 합니다.

실패한 단계 재시도 및 영구 오류 처리, 조건부 쓰기로 중복 주문 방지, Lambda 함수 설정 및 진단을 먼저 완료하세요. 이 독립적인 VM에는 보호되지 않은 작업자가 제공됩니다. 방지 장치는 직접 추가합니다.

인증 시험 관련 주제

이 실습은 다음 시험 주제에 대한 실습 경험을 제공합니다.

안정적인 키로 비즈니스 쓰기 보호

이 단계에서는 같은 주문의 반복을 이미 완료된 쓰기로 취급하는 작업자를 배포합니다.

두 실행과 비즈니스 주문 하나

서로 다른 실행 이름이 같은 비즈니스 주문을 참조할 수 있습니다. 동일한 내용의 조건부 쓰기 충돌은 주문을 교체하지 않고 중복 결과를 반환합니다.

Terminal 옆에서 AWS View를 사용하여 CLI 쿼리를 이 실습의 실제 리소스 및 결과와 비교하세요. 제공된 참조 데이터를 유지하세요.

주문 ID는 비즈니스 키입니다. 작업 시도나 워크플로 실행 이름이 달라도 의도한 주문을 식별합니다. attribute_not_exists(id)는 첫 PutItem만 허용합니다. DynamoDB는 해당 조건을 원자적으로 평가합니다. 재시도가 조건 경쟁에서 실패하면 ReturnValuesOnConditionCheckFailure:ALL_OLD가 기존 항목을 제공합니다. 해당 기존 항목이 의도한 주문과 같을 때만 중복 결과를 반환하세요. 내용이 충돌하면 주문을 조용히 교체하지 않고 계속 실패해야 합니다.

제공된 시작 코드에는 무조건 쓰기가 있습니다. 아래의 전체 보호된 핸들러로 교체하세요. 개인 자격 증명이 아닌 설정된 SDK 환경을 사용합니다. 진단 시도는 실제 호출을 비즈니스 쓰기와 별도로 추적합니다. 테스트용 after-write 모드에서 첫 시도는 네이티브 쓰기 후 오류를 발생시켜 재시도가 처리해야 할 불확실성을 만듭니다. summary 모드는 실제 저장된 항목을 읽습니다.

따옴표가 있는 here-document는 리터럴 Python 파일을 씁니다. 기존 Lambda 핸들러는 app.handler로 유지됩니다.

cd /home/labex/project
cat > app.py <<'PYTHON'
import json
import os
import boto3
from botocore.exceptions import ClientError

class TransientOrderError(Exception):
    pass
class InvalidOrder(Exception):
    pass

def handler(event, context):
    print('WORKFLOW '+json.dumps(event,sort_keys=True))
    db=boto3.client('dynamodb')
    if event.get('stage')=='summary':
        item=db.get_item(
            TableName=os.environ['ORDERS_TABLE'],
            Key={'id':{'S':event['id']}},
        )['Item']
        result={'id':event['id'],'total_cents':int(item['total_cents']['N']),'completed':True}
        print('RESULT '+json.dumps(result,sort_keys=True))
        return result
    if event.get('mode')=='permanent':
        raise InvalidOrder('Order cannot be completed')
    quantity=event['quantity']
    if isinstance(quantity,bool) or not isinstance(quantity,int) or not 1<=quantity<=10:
        raise InvalidOrder('Invalid quantity')
    attempt=db.update_item(
        TableName=os.environ['ATTEMPTS_TABLE'],
        Key={'id':{'S':event['id']}},
        UpdateExpression='ADD attempts :one',
        ExpressionAttributeValues={':one':{'N':'1'}},
        ReturnValues='UPDATED_NEW',
    )['Attributes']['attempts']['N']
    if event.get('mode')=='flaky' and attempt=='1':
        raise TransientOrderError('Dependency temporarily unavailable')
    item={'id':{'S':event['id']},'quantity':{'N':str(quantity)},'total_cents':{'N':str(quantity*250+100)}}
    duplicate=False
    try:
        db.put_item(
            TableName=os.environ['ORDERS_TABLE'],
            Item=item,
            ConditionExpression='attribute_not_exists(id)',
            ReturnValuesOnConditionCheckFailure='ALL_OLD',
        )
    except ClientError as error:
        if (
            error.response['Error']['Code']!='ConditionalCheckFailedException'
            or error.response.get('Item')!=item
        ):
            raise
        duplicate=True
    if event.get('mode')=='after-write' and attempt=='1':
        raise TransientOrderError('Result delivery failed after the write')
    result={'id':event['id'],'quantity':quantity,'total_cents':quantity*250+100,'duplicate':duplicate}
    print('RESULT '+json.dumps(result,sort_keys=True))
    return result
PYTHON

try는 네이티브 조건부 쓰기를 수행합니다. 동일한 이전 항목을 포함하는 실제 ConditionalCheckFailedException만 중복으로 취급합니다. 다른 SDK 오류는 다시 발생시킵니다. 일시적 예외는 쓰기 결정 이후에 발생하므로 워크플로는 비즈니스 항목이 이미 존재하는 작업을 실제로 재시도합니다.

디렉터리 경로를 제외하는 zip -j로 파일을 패키징하고 바이너리 아카이브 바이트를 위한 --zip-file fileb://로 제공된 작업자를 업데이트하세요. 응답의 CodeSha256은 배포된 아카이브를 식별합니다. 배포만으로 중복 보호를 증명할 수는 없습니다. 런타임 단계에서 테스트합니다.

zip -j function.zip app.py
aws lambda update-function-code \
  --function-name labex-ev05-worker \
  --zip-file fileb://function.zip \
  --query CodeSha256 \
  --output text

실행을 만들기 전에 배포 검사를 수행하세요.

제한된 재시도와 범위가 제한된 워크플로 연결

이 단계에서는 작업자의 쓰기 후 실패를 재시도할 수 있는 독립적인 워크플로 역할과 머신을 만듭니다.

Step Functions에는 states 서비스 신뢰와 정확한 함수에 대한 별도의 InvokeFunction 권한이 필요합니다. 작업자는 자체 DynamoDB 권한을 유지하며 워크플로 역할은 호출만 합니다. 셸 할당은 반환된 식별자를 저장합니다. --query와 --output text는 재사용 가능한 값을 선택하고 file://는 리터럴 신뢰 JSON을 읽으며 셸은 작업자 ARN을 권한 문서에 넣습니다.

cd /home/labex/project
WORKER_NAME=labex-ev05-worker
WORKER_ARN=$(aws lambda get-function-configuration \
  --function-name labex-ev05-worker \
  --query FunctionArn \
  --output text)
aws lambda get-function-configuration \
  --function-name labex-ev05-worker \
  --query '{Name:FunctionName,Role:Role,Runtime:Runtime,Timeout:Timeout}'
aws stepfunctions list-state-machines
cat > workflow-trust.json <<'JSON'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "states.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
JSON
ROLE_ARN=$(aws iam create-role \
  --role-name labex-ev05-workflow-role \
  --assume-role-policy-document file://workflow-trust.json \
  --query Role.Arn \
  --output text)
cat > workflow-invoke.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "lambda:InvokeFunction",
      "Resource": "$WORKER_ARN"
    }
  ]
}
EOF
aws iam put-role-policy \
  --role-name labex-ev05-workflow-role \
  --policy-name InvokeWorker \
  --policy-document file://workflow-invoke.json
aws iam get-role-policy --role-name labex-ev05-workflow-role --policy-name InvokeWorker

빈 머신 목록은 이전 VM 워크플로를 재사용하지 않음을 확인합니다. 권한은 함수 ARN을 사용하고 각 Task는 일반 함수 이름을 사용합니다. Retry는 TransientOrderError만 처리하며 최초 시도 후 1초 간격, 두 배로 늘어나는 백오프와 최대 두 번의 재시도를 사용합니다. Catch는 InvalidOrder를 명시적인 Fail로 라우팅합니다. ResultSelector는 실제 Payload를 유지하고 ResultPath는 이를 saved 아래에 보관하며 OutputPath는 실제 요약을 반환합니다.

따옴표가 있는 here-document는 리터럴 ASL을 씁니다. jq --arg는 머신 생성 전에 작업자 자리 표시자를 교체합니다.

cat > workflow-template.json <<'JSON'
{
  "StartAt": "CheckQuantity",
  "States": {
    "CheckQuantity": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.quantity",
          "NumericGreaterThan": 0,
          "Next": "StoreOrder"
        }
      ],
      "Default": "Rejected"
    },
    "StoreOrder": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "WORKER_NAME",
        "Payload.$": "$"
      },
      "ResultSelector": {
        "result.$": "$.Payload"
      },
      "ResultPath": "$.saved",
      "Retry": [
        {
          "ErrorEquals": [
            "TransientOrderError"
          ],
          "IntervalSeconds": 1,
          "BackoffRate": 2,
          "MaxAttempts": 2
        }
      ],
      "Catch": [
        {
          "ErrorEquals": [
            "InvalidOrder"
          ],
          "Next": "Rejected",
          "ResultPath": "$.failure"
        }
      ],
      "Next": "ReadSummary"
    },
    "ReadSummary": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "WORKER_NAME",
        "Payload": {
          "stage": "summary",
          "id.$": "$.saved.result.id"
        }
      },
      "OutputPath": "$.Payload",
      "End": true
    },
    "Rejected": {
      "Type": "Fail",
      "Error": "OrderRejected",
      "Cause": "Order could not be completed"
    }
  }
}
JSON
jq --arg worker "$WORKER_NAME" '.States.StoreOrder.Parameters.FunctionName=$worker | .States.ReadSummary.Parameters.FunctionName=$worker' workflow-template.json > workflow.json
MACHINE_ARN=$(aws stepfunctions create-state-machine \
  --name labex-ev05-orders \
  --type STANDARD \
  --role-arn "$ROLE_ARN" \
  --definition file://workflow.json \
  --query stateMachineArn \
  --output text)
aws stepfunctions describe-state-machine \
  --state-machine-arn "$MACHINE_ARN" \
  --query '{Name:name,Definition:definition}'

네이티브 정의에는 선택적인 Retry/Catch와 두 실제 작업이 나열됩니다. AWS View에는 아직 실행이나 주문이 없는 머신이 표시됩니다. 워크플로 설정 검사를 실행하세요.

재시도와 재실행에 걸친 비즈니스 쓰기 하나 증명

이 단계에서는 쓰기 후 실패를 실행하고 같은 주문을 반복하며 다른 주문을 만듭니다.

실행 이름은 워크플로 실행을 식별하고 주문 ID는 비즈니스 효과를 식별합니다. 첫 실행은 after-write 모드를 사용합니다. 횟수가 제한된 루프는 RUNNING 상태가 끝날 때까지 상태를 읽습니다. $(...)는 출력을 저장하고 break는 루프를 종료합니다. 루프 이후에도 RUNNING이면 실행을 확인하세요.

FIRST_ARN=$(aws stepfunctions start-execution \
  --state-machine-arn "$MACHINE_ARN" \
  --name after-write-order \
  --input '{"id":"protected-order","quantity":4,"mode":"after-write"}' \
  --query executionArn \
  --output text)
for attempt in $(seq 1 60); do
  STATUS=$(aws stepfunctions describe-execution \
    --execution-arn "$FIRST_ARN" \
    --query status \
    --output text)
  if test "$STATUS" != RUNNING; then break; fi
  sleep 2
done
aws stepfunctions describe-execution \
  --execution-arn "$FIRST_ARN" \
  --query '{Status:status,Output:output}'
aws stepfunctions get-execution-history \
  --execution-arn "$FIRST_ARN" \
  --query 'events[?type==`TaskFailed` || type==`TaskSucceeded`].{Type:type,Error:taskFailedEventDetails.error,Output:taskSucceededEventDetails.output}'
aws dynamodb get-item \
  --table-name labex-ev05-orders \
  --key '{"id":{"S":"protected-order"}}' \
  --query Item

첫 TaskFailed는 실제 쓰기 이후의 TransientOrderError입니다. 성공한 재시도는 duplicate:true를 반환하며 요약은 합계 1100으로 완료됩니다. protected-order 항목 하나의 수량은 4, 합계는 1100입니다. 작업 재시도는 해당 비즈니스 키에 대한 두 번째 허용된 PutItem 없이 성공했습니다.

같은 주문 ID와 값으로 새 실행 이름을 시작하세요.

REPEAT_ARN=$(aws stepfunctions start-execution \
  --state-machine-arn "$MACHINE_ARN" \
  --name repeated-order \
  --input '{"id":"protected-order","quantity":4,"mode":"after-write"}' \
  --query executionArn \
  --output text)
for attempt in $(seq 1 60); do
  STATUS=$(aws stepfunctions describe-execution \
    --execution-arn "$REPEAT_ARN" \
    --query status \
    --output text)
  if test "$STATUS" != RUNNING; then break; fi
  sleep 2
done
aws stepfunctions describe-execution \
  --execution-arn "$REPEAT_ARN" \
  --query '{Status:status,Output:output}'
aws stepfunctions get-execution-history \
  --execution-arn "$REPEAT_ARN" \
  --query 'events[?type==`TaskSucceeded`].taskSucceededEventDetails.output'

이 새 실행도 중복 저장 결과를 반환하고 원래의 1100 요약을 읽습니다. 새 실행 이름이 주문을 새 비즈니스 요청으로 만들지는 않습니다. 실제 조건 실패는 원래 항목을 덮어쓰는 대신 유지합니다.

방지 장치가 관련 없는 작업을 거부하지 않음을 증명하기 위해 다른 주문 ID를 사용하세요.

NEW_ARN=$(aws stepfunctions start-execution \
  --state-machine-arn "$MACHINE_ARN" \
  --name different-order \
  --input '{"id":"different-order","quantity":1,"mode":"normal"}' \
  --query executionArn \
  --output text)
for attempt in $(seq 1 60); do
  STATUS=$(aws stepfunctions describe-execution \
    --execution-arn "$NEW_ARN" \
    --query status \
    --output text)
  if test "$STATUS" != RUNNING; then break; fi
  sleep 2
done
aws stepfunctions describe-execution \
  --execution-arn "$NEW_ARN" \
  --query '{Status:status,Output:output}'
aws dynamodb scan --table-name labex-ev05-orders --query Items
aws logs filter-log-events \
  --log-group-name /aws/lambda/labex-ev05-worker \
  --query 'events[].message'

다른 주문은 1/350으로 성공합니다. 저장 시도와 요약 읽기에 걸쳐 실제 작업자 호출은 일곱 번 발생했지만 비즈니스 항목은 두 개입니다. 로그와 네이티브 작업 결과는 원래 키에 대한 중복 결정을 보여 줍니다. AWS View는 세 성공 요약과 유지된 두 주문을 보여 줍니다.

아래 예시는 재시도와 재실행의 실제 요약을 유지된 원래 주문과 별도의 새 주문과 함께 보여 줍니다.

유지된 원래 주문과 다른 비즈니스 주문이 표시된 AWS View 이는 테스트한 동일 요청에 대한 멱등적인 쓰기이며 작업이 정확히 한 번 실행된다는 약속은 아닙니다.

비즈니스 보호 검사를 실행하세요.

워크플로 리소스와 테스트 결과 제거

이 단계에서는 제공된 준비물을 유지하면서 완료된 머신, 워크플로 역할, 주문과 로그를 삭제합니다.

세 실행은 모두 종료되었습니다. 머신을 삭제하면 활성 머신 목록에서 제거됩니다. 역할을 삭제하기 전에 소유한 역할 정책을 제거한 다음 실행으로 만든 테스트 주문과 작업자 로그 그룹을 제거하세요.

aws stepfunctions delete-state-machine --state-machine-arn "$MACHINE_ARN"
aws iam delete-role-policy --role-name labex-ev05-workflow-role --policy-name InvokeWorker
aws iam delete-role --role-name labex-ev05-workflow-role
aws dynamodb delete-item \
  --table-name labex-ev05-orders \
  --key '{"id":{"S":"protected-order"}}'
aws dynamodb delete-item \
  --table-name labex-ev05-attempts \
  --key '{"id":{"S":"protected-order"}}'
aws dynamodb delete-item \
  --table-name labex-ev05-orders \
  --key '{"id":{"S":"different-order"}}'
aws dynamodb delete-item \
  --table-name labex-ev05-attempts \
  --key '{"id":{"S":"different-order"}}'
aws logs delete-log-group --log-group-name /aws/lambda/labex-ev05-worker

성공한 인벤토리를 읽어 남은 것을 증명하세요.

aws stepfunctions list-state-machines
aws iam list-roles --query 'Roles[].RoleName'
aws dynamodb scan --table-name labex-ev05-orders --query Items
aws logs describe-log-groups --query logGroups
aws dynamodb scan --table-name labex-ev05-reference --query Items

활성 머신, 주문이나 로그 그룹은 없습니다. 제공된 작업자 역할만 남으며 참조 항목은 변경되지 않습니다. 제공된 작업자와 테이블을 유지하세요. 준비 단계의 소유 범위는 직접 만든 워크플로/리소스와 다릅니다. 네트워크 오류나 인증 오류는 삭제를 증명하지 않습니다.

이 실습에서 만든 일반 파일을 제거하세요.

rm -f app.py function.zip workflow-trust.json workflow-invoke.json workflow-template.json workflow.json

AWS View는 빈 머신/실행/주문과 유지된 참조를 보여 줍니다. VM을 종료하기 전에 정리 검사를 실행하세요.

요약

안정적인 주문 키로 네이티브 비즈니스 쓰기를 보호하고 동일한 조건부 충돌만 중복으로 취급했으며 첫 쓰기 이후의 실제 실패를 테스트했습니다. 재시도와 새 워크플로 실행은 원래 주문을 유지했고 다른 비즈니스 키는 자체 결과를 생성했습니다. 소유한 워크플로 리소스와 테스트 결과를 제거하기 전에 기록과 네이티브 데이터를 검증했습니다.