성능 튜닝 및 테스트 속도
GitLab v19.4요약
API 보안 테스트와 같이 동적 분석 테스트를 수행하는 보안 도구는 실행 중인 애플리케이션 인스턴스에 요청을 보내는 방식으로 테스트합니다. 이 성능 가이드의 권고를 따랐는데도 API 보안 테스트 job 이 예상보다 오래 걸린다면, 추가 지원을 받기 위해 지원팀에 문의합니다.
API 보안 테스트와 같이 동적 분석 테스트를 수행하는 보안 도구는 실행 중인 애플리케이션 인스턴스에 요청을 보내는 방식으로 테스트합니다. 이 요청은 애플리케이션에 존재할 수 있는 특정 취약점을 검사하도록 설계되어 있습니다. 동적 분석 테스트의 속도는 다음 요소에 따라 달라집니다.
- GitLab 도구가 애플리케이션에 초당 보낼 수 있는 요청 수
- 애플리케이션이 요청에 응답하는 속도
- 애플리케이션을 테스트하기 위해 보내야 하는 요청 수
- API를 구성하는 작업 수
- 각 작업에 포함된 필드 수(JSON 본문, 헤더, 쿼리 문자열, 쿠키 등)
이 성능 가이드의 권고를 따랐는데도 API 보안 테스트 job 이 예상보다 오래 걸린다면, 추가 지원을 받기 위해 지원팀에 문의합니다.
성능 문제 진단#
성능 문제를 해결하는 첫 단계는 테스트 시간이 예상보다 길어지는 원인을 파악하는 것입니다. 자주 보고되는 원인은 다음과 같습니다.
- API 보안 테스트가 vCPU가 적은 러너에서 실행되는 경우
- 애플리케이션이 느리거나 단일 CPU 인 인스턴스에 배포되어 테스트 부하를 감당하지 못하는 경우
- 전체 테스트 속도에 영향을 미치는 느린 작업이 애플리케이션에 있는 경우(> 1/2초)
- 대량의 데이터를 반환하는 작업이 애플리케이션에 있는 경우(> 500K 이상)
- 애플리케이션에 작업이 많은 경우(> 40)
전체 테스트 속도에 영향을 미치는 느린 작업이 있는 경우 (> 1/2초)#
API 보안 테스트 job 출력에는 테스트 속도, 작업 응답 시간, 요약 정보 등 유용한 정보가 포함되어 있습니다. 다음 샘플 출력은 성능 문제를 추적할 때 요약 출력을 어떻게 활용할 수 있는지 보여 줍니다.
API SECURITY: Loaded 10 operations from: assets/har-large-response/large_responses.har
API SECURITY:
API SECURITY: Testing operation [1/10]: 'GET http://target:7777/api/large_response_json'.
API SECURITY: - Parameters: (Headers: 4, Query: 0, Body: 0)
API SECURITY: - Request body size: 0 Bytes (0 bytes)
API SECURITY:
API SECURITY: Finished testing operation 'GET http://target:7777/api/large_response_json'.
API SECURITY: - Excluded Parameters: (Headers: 0, Query: 0, Body: 0)
API SECURITY: - Performed 767 requests
API SECURITY: - Average response body size: 130 MB
API SECURITY: - Average call time: 2 seconds and 82.69 milliseconds (2.082693 seconds)
API SECURITY: - Time to complete: 14 minutes, 8 seconds and 788.36 milliseconds (848.788358 seconds)
이 job 콘솔 출력 조각은 발견된 작업 수(10개)로 시작합니다. 이어서 특정 작업의 테스트가 시작되었음을 알리고, 해당 작업의 요약이 완료되었음을 알립니다. 요약을 보면 API 보안 테스트가 이 작업과 관련 필드를 완전히 테스트하는 데 767건의 요청을 사용했음을 알 수 있습니다. 또한 이 작업의 평균 응답 시간이 2초였고 완료까지 14분이 걸렸음을 보여 줍니다.
평균 응답 시간 2초는 이 특정 작업의 테스트가 오래 걸린다는 점을 알려 주는 좋은 초기 지표입니다. 요약에는 응답 본문 크기가 크다는 점도 나타나며, 이것이 긴 응답 시간의 원인입니다. 각 요청의 응답 시간 대부분이 응답 본문 데이터를 전송하는 데 쓰입니다.
이 문제에 대해 팀은 다음과 같이 결정할 수 있습니다.
- vCPU가 더 많은 러너를 사용합니다. API 보안 테스트가 작업을 병렬로 처리할 수 있기 때문입니다. 테스트 시간을 줄이는 데 도움이 되지만, 이 작업의 테스트 소요 시간 때문에 고성능 CPU 머신으로 옮기지 않으면 10분 이내로 낮추기는 어려울 수 있습니다. 큰 러너는 비용이 더 들지만, job 실행이 빨라지면 청구되는 분 수는 줄어듭니다.
- API 보안 테스트에서 이 작업을 제외합니다. 가장 간단한 방법이지만 보안 테스트 커버리지에 공백이 생긴다는 단점이 있습니다.
- 피처 브랜치의 API 보안 테스트에서는 해당 작업을 제외하고 기본 브랜치 테스트에는 포함합니다.
- API 보안 테스트를 여러 job으로 분할합니다.
팀의 요구 사항이 5~7분 범위라고 가정하면, 허용 가능한 테스트 시간에 도달하려면 이 방법들을 조합해 사용하는 것이 현실적인 해법입니다.
성능 문제 해결#
다음 섹션에서는 API 보안 테스트의 성능 문제를 해결하는 여러 방법을 설명합니다.
더 큰 러너 사용#
가장 손쉽게 성능을 높이는 방법 하나는 API 보안 테스트에 더 큰 러너를 사용하는 것입니다. 다음 표는 Java Spring Boot REST API를 벤치마킹하면서 수집한 통계입니다. 이 벤치마크에서는 타깃과 API 보안 테스트가 단일 러너 인스턴스를 공유합니다.
| Linux의 호스팅 러너 태그 | 초당 요청 수 |
|---|---|
saas-linux-small-amd64 (기본) |
255 |
saas-linux-medium-amd64 |
400 |
이 표는 러너 크기와 vCPU 수를 늘리는 것이 테스트 속도와 성능에 큰 영향을 줄 수 있음을 보여 줍니다.
다음은 Linux 용 medium GitLab 호스팅 러너를 사용하도록 tags 섹션을 추가한 API 보안 테스트 job 정의 예시입니다. 이 job은 API 보안 테스트 템플릿에 포함된 job 정의를 확장합니다.
api_security:
tags:
- saas-linux-medium-amd64
gl-api-security-scanner.log 파일에서 Starting work item processor 문자열을 검색하면 보고된 최대 DOP(병렬 처리 수준)를 확인할 수 있습니다. 최대 DOP는 러너에 할당된 vCPU 수보다 크거나 같아야 합니다. 문제를 파악하기 어렵다면 지원팀에 티켓을 열어 도움을 요청합니다.
로그 항목 예시:
17:00:01.084 [INF] Starting work item processor with 4 max DOP
느린 작업 제외#
느린 작업이 한두 개인 경우, 팀은 해당 작업의 테스트를 건너뛰기로 결정할 수 있습니다. 작업 제외는 APISEC_EXCLUDE_PATHS 구성 변수로 수행하며 자세한 내용은 이 섹션에서 설명합니다.
다음 예시는 대량의 데이터를 반환하는 작업을 보여 줍니다. 해당 작업은 GET http://target:7777/api/large_response_json 입니다. 이 작업을 제외하려면 APISEC_EXCLUDE_PATHS 구성 변수에 작업 URL의 경로 부분인 /api/large_response_json을 지정합니다.
작업이 제외되었는지 확인하려면 API 보안 테스트 job을 실행하고 job 콘솔 출력을 검토합니다. 테스트 마지막에 포함된 작업과 제외된 작업 목록이 표시됩니다.
api_security:
variables:
APISEC_EXCLUDE_PATHS: /api/large_response_json
테스트에서 작업을 제외하면 일부 취약점이 탐지되지 않을 수 있습니다.
테스트를 여러 job으로 분할#
API 보안 테스트는 APISEC_EXCLUDE_PATHS와 APISEC_EXCLUDE_URLS를 사용해 테스트를 여러 job으로 분할하는 것을 지원합니다. 테스트를 분할할 때는 dast_api job을 비활성화하고 식별 가능한 이름을 가진 두 개의 job으로 대체하는 방식이 좋습니다. 다음 예시는 두 개의 job을 보여 줍니다. 각 job은 이름에서 알 수 있듯이 API의 각 버전을 테스트합니다. 다만 이 기법은 API 버전에 한정되지 않고 어떤 상황에나 적용할 수 있습니다.
APISEC_v1과 APISEC_v2 job에 사용된 rules는 API 보안 테스트 템플릿에서 복사한 것입니다.
# Disable the main dast_api job
api_security:
rules:
- if: $CI_COMMIT_BRANCH
when: never
APISEC_v1:
extends: dast_api
variables:
APISEC_EXCLUDE_PATHS: /api/v1/**
rules:
- if: $APISEC_DISABLED == 'true' || $APISEC_DISABLED == '1'
when: never
- if: $APISEC_DISABLED_FOR_DEFAULT_BRANCH == 'true' &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- if: $APISEC_DISABLED_FOR_DEFAULT_BRANCH == '1' &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- if: $CI_COMMIT_BRANCH &&
$CI_GITLAB_FIPS_MODE == "true"
variables:
APISEC_IMAGE_SUFFIX: "-fips"
- if: $CI_COMMIT_BRANCH
APISEC_v2:
variables:
APISEC_EXCLUDE_PATHS: /api/v2/**
rules:
- if: $APISEC_DISABLED == 'true' || $APISEC_DISABLED == '1'
when: never
- if: $APISEC_DISABLED_FOR_DEFAULT_BRANCH == 'true' &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- if: $APISEC_DISABLED_FOR_DEFAULT_BRANCH == '1' &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- if: $CI_COMMIT_BRANCH &&
$CI_GITLAB_FIPS_MODE == "true"
variables:
APISEC_IMAGE_SUFFIX: "-fips"
- if: $CI_COMMIT_BRANCH
기본 브랜치가 아닌 피처 브랜치에서 작업 제외#
느린 작업이 한두 개인 경우, 팀은 해당 작업의 테스트를 건너뛰거나 피처 브랜치 테스트에서는 제외하고 기본 브랜치 테스트에는 포함하기로 결정할 수 있습니다. 작업 제외는 APISEC_EXCLUDE_PATHS 구성 변수로 수행하며 자세한 내용은 이 섹션에서 설명합니다.
다음 예시는 대량의 데이터를 반환하는 작업을 보여 줍니다. 해당 작업은 GET http://target:7777/api/large_response_json 입니다. 이 작업을 제외하려면 APISEC_EXCLUDE_PATHS 구성 변수에 작업 URL의 경로 부분인 /api/large_response_json을 지정합니다. 이 구성은 기본 dast_api job을 비활성화하고 APISEC_main과 APISEC_branch라는 두 개의 새 job을 만듭니다. APISEC_branch는 오래 걸리는 작업을 제외하고 기본 브랜치가 아닌 브랜치(예: 피처 브랜치)에서만 실행되도록 설정합니다. APISEC_main 브랜치는 기본 브랜치(이 예시에서는 main)에서만 실행되도록 설정합니다. APISEC_branch job은 더 빠르게 실행되어 개발 주기를 단축하고, 기본 브랜치 빌드에서만 실행되는 APISEC_main job은 실행 시간이 더 깁니다.
작업이 제외되었는지 확인하려면 API 보안 테스트 job을 실행하고 job 콘솔 출력을 검토합니다. 테스트 마지막에 포함된 작업과 제외된 작업 목록이 표시됩니다.
# Disable the main job so you can create two jobs with
# different names
api_security:
rules:
- if: $CI_COMMIT_BRANCH
when: never
# API security testing for feature branch work, excludes /api/large_response_json
APISEC_branch:
extends: dast_api
variables:
APISEC_EXCLUDE_PATHS: /api/large_response_json
rules:
- if: $APISEC_DISABLED == 'true' || $APISEC_DISABLED == '1'
when: never
- if: $APISEC_DISABLED_FOR_DEFAULT_BRANCH == 'true' &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- if: $APISEC_DISABLED_FOR_DEFAULT_BRANCH == '1' &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- if: $CI_COMMIT_BRANCH &&
$CI_GITLAB_FIPS_MODE == "true"
variables:
APISEC_IMAGE_SUFFIX: "-fips"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: never
- if: $CI_COMMIT_BRANCH
# API security testing for default branch (main in this case)
# Includes the long running operations
APISEC_main:
extends: dast_api
rules:
- if: $APISEC_DISABLED == 'true' || $APISEC_DISABLED == '1'
when: never
- if: $APISEC_DISABLED_FOR_DEFAULT_BRANCH == 'true' &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- if: $APISEC_DISABLED_FOR_DEFAULT_BRANCH == '1' &&
$CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
when: never
- if: $CI_COMMIT_BRANCH &&
$CI_GITLAB_FIPS_MODE == "true"
variables:
APISEC_IMAGE_SUFFIX: "-fips"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH