[극한의 자동화] Ngrinder + AWS + Github action + springboot

2022. 9. 14. 02:07기타





토이프로젝트 중, 홍대바람이 불었는지 이상한 자동화를 하고 있습니다.

성능테스트가 좋은 것은 알겠는데, 너무 번거롭습니다.
개발자가 신경써야할 부분은 두 가지 여야 합니다.

  1. 어플리케이션 코드를 작성
  2. 작성된 코드의 성능 지표를 확인.


프로젝트를 빌드하고, 새롭게 배포하고, 이에 맞춰 인프라를 수동으로 늘리고, 테스트를 설정, 생성, 시작시키고, 결과를 조회하고, 인프라를 다시 롤백시키는 등의 오버헤드가 너무 크다고 판단되었습니다.

그래서 이 모든 것을 처리해주는 친구를 만들기로 했습니다.




깃허브에서 run workflow 버튼을 누르면,

  • 빌드
  • 빌드 결과물 도커에 푸시
  • aws cli 를 통해 성능테스트에 필요한 인스턴스를 모두 시작 상태로 trigger
  • trigger 된 인스턴스 (Controller, Agent, Test_Server) 마다 미리 작성된 스크립트를 실행함
    • Controller는 Ngrinder-Controller를 도커를 통해 설정 및 실행
    • Agent는 Ngrinder-Agent 설정 및 실행
    • Server(test) 는 앞서 도커에 푸시된 스프링서버 이미지를 pull 후 deploy
  • Ngrinder Rest-API 를 이용해 미리 작성된 스크립트로부터 성능테스트 생성 -> 실행
  • 성능 테스트가 끝나면 결과를 조회하고, 조회된 리소스를 개발자 개인 email, slack 등으로 전송
  • 생성한 자원 및 인프라를 정리



쓰고나니 별 것 아닌 것 같네요. 하지만 말하는 감자인 제게는 위 한 줄 한 줄이 모두 엄청난 시련이었습니다.
또, 이상한 고집으로 AWS elastic IP는 치팅인 기분이 들어서 동적으로 ip를 땡겨오고 밀어주고 다시 받아와서 설정하고... 하다가 중간에 러시앤캐시 방문해서 돈 빌릴 뻔 했습니다.. 돈만 있었다면 머리가 고생하지 않았을 텐데..

Ngrinder 부하테스트와 github action, AWS 인프라 (ec2, rds), 스프링부트 설정 꼬임까지 어느 하나 쉬운 것이 없었습니다.
특히, 자료가 좀 적습니다....... Ngrinder 자료가 없어요..... 중국인 개발자 페이지가 어떻게 되어있는지 아십니까? 저는 알고 있습니다... 어떻게 알았냐고요? 저도 알고 싶지 않았습니다..........

삽질은 지긋지긋한데도, 삽질하는 과정에서 깊게 집중하는 스스로의 모습이 대견해서 몇 주째 몰두하고 있습니다. 아직 오토 스케일링, vpc, 시나리오 재정립 등 작업이 모두 완전히 끝난 것은 아니지만, 없는 자료를 찾아 헤맬 누군가에게 이 글을 바칩니다..

- 시 작 -



일단 자동화는 제쳐두고, 수동으로라도 서비스 성능 테스트를 하려면 테스트 스크립트는 당연히 준비되어 있어야 합니다.

Ngrinder가 아직 익숙하지 않다면, 제가 미천한 실력으로 튜토리얼을 작성해보았습니다..
https://leezzangmin.tistory.com/42
튜토리얼 정도의 아티클은 꽤 많으니 제 똥글 말고 유명한 글 보시면 되겠습니다


필요한 것은 크게 4가지가 있습니다.

  1. AWS 인프라(ec2, rds) 및 각각의 실행 스크립트 (bash)
  2. Github Action 스크립트 (yml)
  3. 서비스 어플리케이션 코드 github 저장소
  4. Ngrinder 스크립트 (groovy)






우선 AWS 설정 먼저 살펴 보도록 하겠습니다.

IAM 사용자가 이미 만들어져 있다고 가정합니다.

 

https://aws.plainenglish.io/turn-on-aws-ec2-using-github-actions-a225820bbe9f

 

여길 보시면 도움이 될 수도 있습니다.

 

 

t3a.medium

우선 최소 3개의 EC2 인스턴스가 필요했습니다.

성능 테스트 Controller 와 Agent, 테스트서버로 이루어집니다.


단순히 스크립트 내에서만 잠깐 켜졌다가 꺼지는 인스턴스라서 보안 설정을 넉넉히 잡아둬도 큰 문제가 되지 않으리라 예상됩니다.
Ngrinder Controller inbound - (22, 80, 12000 - 12009, 16001), outbound - 전체
Ngrinder Agent inbound - (22, 12000 - 12009, 16001), outboud - 전체
Test Server inbound (22, 80), outbound - 전체

모두 아마존 리눅스 환경에서 진행했습니다.
Java 11과 Docker는 모두 미리 설치 - 설정해두었습니다.

 

Ngrinder Controller와 Agent는 미리 aws configure 작업을 수행해야 합니다.

 

  • Controller


ngrinder controller를 도커를 통해 땡겨옵니다.

sudo docker pull ngrinder/controller



다음으로, bashrc에 환경변수를 추가해줍니다.
aws cli를 통해서 test spring 서버의 public ip를 환경변수로 추가하는 명령입니다.

export EC2_SERVER_IP=$(aws ec2 describe-instances --instance-ids {인스턴스id} --query 'Reservations[].Instances[].PublicIpAddress' --outp$



이제 컨트롤러 실행 스크립트를 간단히 작성해봅니다. (controller_run.sh)
github action에서 매 번 실행될 스크립트입니다.
도커를 재시작하고, 이미지를 실행합니다.


우선 초기 설정을 위해서 웹으로 접속해야하니 chmod +x 로 실행권한을 주고 bash 명령어로 스크립트를 실행해줍니다.

#!/bin/bash
source ~/.bashrc
sudo systemctl restart docker
sudo docker rm $(sudo docker ps -a -q)
sudo docker run -d -e EC2_SERVER_IP=$EC2_SERVER_IP -v ~/ngrinder-controller:/opt/ngrinder-controller -p 80:80 -p 16001:16001 -p 12000-12009:12000-12009 ngrinder/controller



docker logs 로 확인했을때

마지막 줄의 Started 메세지가 나오면 1차 성공


그리고 해당 ec2 인스턴스의 public-ip로 브라우저를 통해 접속했을 때,

이 화면이 보이면 2차 성공


초기 id와 pw는 admin, admin 입니다
미리 작성해둔 스크립트가 있다면, 스크립트 페이지에 들어가서 미리 저장해둡시다.


  • Agent


우선 controller에 연결할 친구니까 미리 환경변수에 controller ec2의 public ip를 등록해놓읍시다.
sudo nano ~/.bashrc

export CONTROLLER_IP=$(aws ec2 describe-instances --instance-ids {컨트롤러인스턴스id} --query 'Reservations[].Instances[].PublicIpAddress' --output text)



ngrinder-agent도 방금 controller와 비슷한 방식으로 도커를 통해 땡겨온 다음 실행하는 방식입니다.

sudo docker pull ngrinder/agent

명령을 실행한 후,

실행 스크립트를 간단하게 작성합니다. (agent_run.sh)

#!/bin/bash
source ~/.bashrc
sudo rm -rf ngrinder-agent
sudo service docker restart
sudo docker run -v ~/ngrinder-agent:/opt/ngrinder-agent -d ngrinder/agent $CONTROLLER_IP:80


스크립트를 실행한 후, Ngrinder의 컨트롤러 웹페이지에서 agent Management 탭으로 들어가줍니다.

에이전트 확인 탭



아래 사진과 같이 무언가 하나 떠있으면 agent 설정까지 성공입니다. 짝짝

성공 시그널

 



이젠 github action 쪽을 살펴봅시다.

Github Action 스크립트:

name: zzangmin_amazing_automation
 
on:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      working-directory: ./gesipan
      test-duration: 120000
      
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'


# test에 필요한 ec2 인스턴스를 aws cli를 통해 실행
      - name: Start Ngrinder EC2 - (Controller, Agent, Test Server)
        run: aws ec2 start-instances --instance-ids ${{ secrets.AWS_EC2_NGRINDER_CONTROLLER }} ${{ secrets.AWS_EC2_NGRINDER_AGENT }} ${{ secrets.AWS_EC2_SPRING_TEST_SERVER }}
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
                                
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
        working-directory: ${{env.working-directory}}

      - name: Build with Gradle
        run: ./gradlew build --stacktrace
        working-directory: ${{env.working-directory}}


      - name: Build and Push Docker Image
        uses: mr-smithers-excellent/docker-build-push@v5
        with:
          image: ${{ secrets.DOCKERHUB_ID }}/springcafe
          tags: latest
          registry: docker.io
          dockerfile: ${{env.working-directory}}/Dockerfile
          username: ${{ secrets.DOCKERHUB_ID }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}


# aws cli를 사용해서 각 인스턴스의 public ip를 환경변수로 등록
      - name: Setting environment variables...
        run: |
          export AWS_EC2_TEST_SERVER_IP=$(aws ec2 describe-instances --instance-ids ${{ secrets.AWS_EC2_SPRING_TEST_SERVER }} --query 'Reservations[].Instances[].PublicIpAddress' --output text)
          export AWS_EC2_SERVER_IP=$(aws ec2 describe-instances --instance-ids ${{ secrets.AWS_EC2_SPRING_SERVER }} --query 'Reservations[].Instances[].PublicIpAddress' --output text)
          export AWS_EC2_CONTROLLER_IP=$(aws ec2 describe-instances --instance-ids ${{ secrets.AWS_EC2_NGRINDER_CONTROLLER }} --query 'Reservations[].Instances[].PublicIpAddress' --output text)
          export AWS_EC2_AGENT_IP=$(aws ec2 describe-instances --instance-ids ${{ secrets.AWS_EC2_NGRINDER_AGENT }} --query 'Reservations[].Instances[].PublicIpAddress' --output text)
          
          echo "$AWS_EC2_TEST_SERVER_IP"
          echo "$AWS_EC2_SERVER_IP"
          echo "$AWS_EC2_CONTROLLER_IP"
          echo "$AWS_EC2_AGENT_IP"
          
          echo "AWS_EC2_TEST_SERVER_IP=$AWS_EC2_TEST_SERVER_IP" >> $GITHUB_ENV
          echo "AWS_EC2_SERVER_IP=$AWS_EC2_SERVER_IP" >> $GITHUB_ENV
          echo "AWS_EC2_CONTROLLER_IP=$AWS_EC2_CONTROLLER_IP" >> $GITHUB_ENV
          echo "AWS_EC2_AGENT_IP=$AWS_EC2_AGENT_IP" >> $GITHUB_ENV
          
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}

# 메인 ec2에 스프링 실행
      - name: Deploy 😆 - main server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ env.AWS_EC2_SERVER_IP }}
          username: ec2-user
          key: ${{ secrets.AWS_SECRET_PEM }}
          script: |
            sudo systemctl restart docker
            sudo docker login --username=${{ secrets.DOCKERHUB_ID }} --password=${{ secrets.DOCKERHUB_PASSWORD }}
            sudo docker pull ${{ secrets.DOCKERHUB_ID }}/springcafe:latest
            sudo docker tag ${{ secrets.DOCKERHUB_ID }}/springcafe:latest server_image
            sudo docker stop server
            sudo docker run -p 80:80 -d --rm -e profile=deploy -e AWS_DB_USERNAME=$AWS_DB_USERNAME -e AWS_DB_PASSWORD=$AWS_DB_PASSWORD -e GITHUB_CLIENT_ID=$GITHUB_CLIENT_ID -e GITHUB_CLIENT_SECRET=$GITHUB_CLIENT_SECRET -e JWT_SECRET=$JWT_SECRET --name server server_image

# 테스트용 ec2에 스프링 실행
      - name: Deploy 😆 - test server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ env.AWS_EC2_TEST_SERVER_IP }}
          username: ec2-user
          key: ${{ secrets.AWS_SECRET_PEM }}
          script: |
            bash deploy.sh          


# EC2- Ngrinder Controller를 실행
      - name: Ngrinder Controller Start
        uses: appleboy/ssh-action@master
        with:
          host: ${{ env.AWS_EC2_CONTROLLER_IP }}
          username: ec2-user
          key: ${{ secrets.AWS_SECRET_PEM }}
          script: |
            bash controller_run.sh
            
# EC2- Ngrinder Agent를 실행
      - name: Ngrinder Agent Start
        uses: appleboy/ssh-action@master
        with:
          host: ${{ env.AWS_EC2_AGENT_IP }}
          username: ec2-user
          key: ${{ secrets.AWS_SECRET_PEM }}
          script: |
            bash ./agent_run.sh
            
# Controller와 Agent가 완전히 실행될 때 까지 기다림
      - name: Sleep for 30 seconds - waiting controller,agent run
        run: sleep 30s
        shell: bash
        
        
# Ngrinder Rest Api 를 통해 성능테스트 생성 - 실행
      - name: HTTP Request Action - Ngrinder rest api perf_test
        uses: fjogeleit/http-request-action@v1
        with:
          url: 'http://${{ env.AWS_EC2_CONTROLLER_IP }}/perftest/api'
          method: 'POST'
          username: 'admin'
          password: 'admin'
          customHeaders: '{"Content-Type": "application/json"}'
          data: '{"param" : "${{ env.AWS_EC2_TEST_SERVER_IP }}", "testName" : "zzangmin_perftest", "tagString" : "spring perf", "description" : "zzangminzzang", "scheduledTime" : "", "useRampUp": false, "rampUpType" : "PROCESS", "threshold" : "D", "scriptName" : "first.groovy", "duration" : ${{ env.test-duration }}, "runCount" : 1, "agentCount" : 1, "vuserPerAgent" : 1, "processes" : 1, "rampUpInitCount" : 0, "rampUpInitSleepTime" : 0, "rampUpStep" : 1, "rampUpIncrementInterval" : 1000, "threads": 1, "testComment" : "제발돼라", "samplingInterval" : 2, "ignoreSampleCount" : 0, "status" : "READY"}'
          timeout: '60000'


# 성능테스트가 진행되는 동안 기다림
      - name: Sleep for 130 seconds - waiting test...
        run: sleep 130s
        shell: bash


# Ngrinder Rest Api 를 통해 테스트 결과 조회
      - name: Get Ngrinder test result ...
        uses: satak/webrequest-action@master
        id: NgrinderTestResult
        with:
          url: 'http://${{ env.AWS_EC2_CONTROLLER_IP }}/perftest/api?page=0'
          method: GET
          username: admin
          password: admin


# 슬랙으로 테스트 결과 전송
      - name: send test result...
        uses: 8398a7/action-slack@v3
        with:
          text: '${{ steps.NgrinderTestResult.outputs.output }}'
          status: ${{ job.status }}
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
        if: always() # Pick up events even if the job fails or is canceled.

# 테스트가 끝난 후 자원들 정리 - aws cli
      - name: Stop Ngrinder EC2 - (Controller, Agent, Test Server)
        run: aws ec2 terminate-instances --instance-ids ${{ secrets.AWS_EC2_NGRINDER_CONTROLLER }} ${{ secrets.AWS_EC2_NGRINDER_AGENT }} ${{ secrets.AWS_EC2_SPRING_TEST_SERVER }}
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}

 

- ngrinder groovy 스크립트

import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.script.GTest
import net.grinder.script.Grinder
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
// import static net.grinder.util.GrinderUtils.* // You can use this if you're using nGrinder after 3.2.3
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith

import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPRequestControl
import org.ngrinder.http.HTTPResponse
import org.ngrinder.http.cookie.Cookie
import org.apache.commons.lang.RandomStringUtils

import org.ngrinder.http.cookie.CookieManager
import java.util.Random
import java.time.LocalDateTime


@RunWith(GrinderRunner)
class TestRunner {

    public static GTest testRecord1
    public static GTest testRecord2
    public static GTest testRecord3
    public static GTest testRecord4
    public static GTest testRecord5
    public static GTest testRecord6
    public static GTest testRecord7
    public static GTest testRecord8
    public static GTest testRecord9
    public static GTest testRecord10
    public static GTest testRecord11
    public static GTest testRecord12
    public static GTest testRecord13

    public static HTTPRequest request = new HTTPRequest()
    public Map<String, String> headers = [:]
    public Map<String, Object> params = [:]
    public List<Cookie> cookies = []
    public Cookie cookie
    public Long temp

//    public static String ip = System.getProperty('param')
        public static String ip = "43.201.71.20"
    public static String host = "http://"+ip



    @BeforeProcess
    public static void beforeProcess() {
                grinder.logger.info("ip !!!"+host)

        HTTPRequestControl.setConnectionTimeout(300000)
        testRecord1 = new GTest(1, ip)
        testRecord2 = new GTest(2, ip)
        testRecord3 = new GTest(3, ip)
        testRecord4 = new GTest(4, ip)
        testRecord5 = new GTest(5, ip)
        testRecord6 = new GTest(6, ip)
        testRecord7 = new GTest(7, ip)
        testRecord8 = new GTest(8, ip)
        testRecord9 = new GTest(9, ip)
        testRecord10 = new GTest(10, ip)
        testRecord11 = new GTest(11, ip)
        testRecord12 = new GTest(12, ip)
        testRecord13 = new GTest(13, ip)
        grinder.logger.info("before process.")
    }

    @BeforeThread
    public void beforeThread() {
        testRecord1.record(this, "test01")
        testRecord2.record(this, "test02")
        testRecord3.record(this, "test03")
        testRecord4.record(this, "test04")
        testRecord5.record(this, "test05")
        testRecord6.record(this, "test06")
        testRecord7.record(this, "test07")
        testRecord8.record(this, "test08")
        testRecord9.record(this, "test09")
        testRecord10.record(this, "test10")
        testRecord11.record(this, "test11")
        testRecord12.record(this, "test12")
        testRecord13.record(this, "test13")

        grinder.statistics.delayReports = true
        grinder.logger.info("before thread.")
    }

    @Before
    public void before() {
        request = new HTTPRequest()
        request.setHeaders(headers)
        CookieManager.addCookies(cookies)
        grinder.logger.info("before. init headers and cookies")
    }

    // 1.로그인 - 2.개인 댓글 조회 - 3.질문게시판 {x}페이지 조회 - 4.질문글 1개 조회 - 5.추천 - 6.댓글남기기 - 7.방금 남긴 댓글 삭제 - 8.자유게시판 {x}페이지 조회 - 9.글 작성 - 10.자유게시판 1페이지 조회 - 11.개인 게시글 조회 - 12.글 수정 - 13. 랜덤 글 조회
    @Test
    public void test01() {
        grinder.logger.info("test1")
        String url1 = host+"/oauth/github-url";
        String url2 = host+"/login?code=this_is_fake_code";

        HTTPResponse response = request.GET(url1, params)
        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }

        HTTPResponse response2 = request.GET(url2, params)
        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
    }

    @Test
    public void test02() {
        grinder.logger.info("test2")
        String url = host+"/comments/my";

        HTTPResponse response = request.GET(url, params)

        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
    }

    @Test
    public void test03() {
        grinder.logger.info("test3")
        String frontURL = host+"/posts?page="
        String backURL = "&size=10&categoryName=QNA&sort=postId,desc"
        String url = frontURL + RandomPostIdIssuer.getRandomNumbers() + backURL;

        HTTPResponse response = request.GET(url, params)
        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
    }

    @Test
    public void test04() {
        grinder.logger.info("test4")
        String url = host+"/post/";
        String randomPostNumber = RandomPostIdIssuer.getRandomNumbers();

        HTTPResponse response = request.GET(url + randomPostNumber, params)

        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
    }

    @Test
    public void test05() {
        grinder.logger.info("test5")
        String url = host+"/post/recommend"
        String postId=RandomPostIdIssuer.getRandomNumbers();
        String userId=RandomPostIdIssuer.getRandomNumbers();
//        String body = "{\"postId\":\""+postId+"\"}"
                String body = postId;
        HTTPResponse response = request.POST(url, body.getBytes())

        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }

    }

    @Test
    public void test06() {
        grinder.logger.info("test6")
        String url = host+"/comment"
        String postId = RandomPostIdIssuer.getRandomNumbers();
        String commentId = RandomPostIdIssuer.getRandomNumbers();
        String charset = (('A'..'Z') + ('0'..'9')).join()
        Integer length = 500
        String commentContent = RandomStringUtils.random(length, charset.toCharArray())
        String body = "{\"referencePostId\":"+postId+",\n \"commentContent\":\""+commentContent+"\",\n \"createdAt\":\""+LocalDateTime.now()+"\"}"
        grinder.logger.info(body)
        HTTPResponse response = request.POST(url, body.getBytes())

        temp = response.getBodyText().toLong()



        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }

    }

    @Test //방금 남긴 댓글 삭제
    public void test07() {
        grinder.logger.info("test7")
        Long deleteCommentId = temp;
        String url = host+"/comment/"+deleteCommentId
        HTTPResponse response = request.DELETE(url,params)
        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
    }

    @Test //자유게시판 조회
    public void test08() {
        grinder.logger.info("test8")
        String frontURL = host+"/posts?page="
        String backURL = "&size=10&categoryName=FREE&sort=postId,desc"
        String url = frontURL + RandomPostIdIssuer.getRandomNumbers() + backURL;

        HTTPResponse response = request.GET(url, params)
        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
    }

    @Test //글작성
    public void test09() {
        grinder.logger.info("test9")
        String url = host+"/post"
        String userId=RandomPostIdIssuer.getRandomNumbers();
        String charset = (('A'..'Z') + ('0'..'9')).join()
        Integer length = 500
        String postSubject = RandomStringUtils.random(30, charset.toCharArray())
        String postContent = RandomStringUtils.random(700, charset.toCharArray())

        String body = "{\"postSubject\":\""+postSubject+"\",\n \"userId\":"+userId+",\n \"postContent\":\""+postContent+"\",\n \"createdAt\":\""+LocalDateTime.now()+"\",\"postCategoryId\":1}"
        grinder.logger.info(body)
        HTTPResponse response = request.POST(url, body.getBytes())
        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
        temp = response.getBodyText().toLong()
        grinder.logger.info("temp!:" + temp)
    }

    @Test
    public void test10() {
        grinder.logger.info("test10")
        String frontURL = host+"/posts?page="
        String backURL = "&size=10&categoryName=FREE&sort=postId,desc"
        String url = frontURL + RandomPostIdIssuer.getRandomNumbers() + backURL;

        HTTPResponse response = request.GET(url, params)
        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
    }

    @Test
    public void test11() {
        grinder.logger.info("test11")
        String url = host+"/posts/my";

        HTTPResponse response = request.GET(url, params)

        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
    }

    @Test //글수정
    public void test12() {
        grinder.logger.info("test12")
        String url = host+"/post/" + temp
        String charset = (('A'..'Z') + ('0'..'9')).join()

        String postSubject = RandomStringUtils.random(30, charset.toCharArray())
        String postContent = RandomStringUtils.random(700, charset.toCharArray())

        String body = "{\"postSubject\":\""+postSubject+"\",\n\"postContent\":\""+postContent+"\",\n \"updatedAt\":\""+LocalDateTime.now()+"\"}"
        HTTPResponse response = request.PATCH(url, body.getBytes())
        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
    }

    @Test
    public void test13() {
        grinder.logger.info("test13")
        String url = host+"/post/";
        String randomPostNumber = RandomPostIdIssuer.getRandomNumbers();

        HTTPResponse response = request.GET(url + randomPostNumber, params)

        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
    }



    class RandomPostIdIssuer {
        private static int NUMBER_LENGTH=4;
        private static int NUMBER_BOUND=10;

        public static String getRandomNumbers(){
            Random random = new Random();
            StringBuilder sb= new StringBuilder();
            for(int i=0; i<NUMBER_LENGTH; i++){
                sb.append(String.valueOf(random.nextInt(NUMBER_BOUND)));
            }
            String temp = sb.toString()
            if(temp.charAt(0)=="0") {
                return RandomPostIdIssuer.getRandomNumbers();
            }
            return temp;
        }
    }
}

 


완성된 스크립트는 이렇습니다. 혹시 작성내용 중 궁금한 내용이 있으시면 댓글 남겨주세요.
고수님들은 스크립트도 객체지향적으로 요렇게 저렇게 트리거 해서 잘 쓰시겠지만...
젊은꼰대 말하는 감자 저는 절차지향적으로 갑니다..

크게 순서를 보자면

  • 환경을 ubuntu-latest로 정의, 작업디렉토리 정의 및 글로벌 환경변수 정의
  • java 11 사용
  • aws cli를 통해 필요한 ec2 인스턴스를 시작상태로 변경 - aws access key id, aws secret access key 필요
  • gradle 실행권한 부여 및 빌드 - 디버깅을 위해 stacktrace 옵션 on
  • 빌드된 결과물을 docker에 push
  • 앞에서 시작상태로 변경한 ec2가 빌드, 푸시되는 동안 슬슬 로딩 되었으니 aws ec2 describe-instaces로 public ip를 환경변수로 등록
  • 환경변수로 등록된 ip를 활용해 서버에 배포 - docker restart -> docker login -> pull -> tag -> 기존것 stop -> run
  • 동일한 방식으로 테스트 서버에도 배포
  • Ngrinder controller를 controller ec2에서 실행 - 실행 스크립트 있음
  • Ngrinder agent를 agent ec2에서 실행 - 실행 스크립트 있음
  • controller와 agent가 실행될 때 까지 30초 sleep
  • Ngrinder Rest api를 이용해서 http 요청으로 성능테스트 생성 -> 실행
  • 성능테스트가 진행되는 동안 sleep 130초 sleep (뭔가 더 좋은 방법이 있을 것 같음)
  • 다시 Ngrinder rest api를 통해 http 요청으로 테스트 결과 조회
  • 조회된 결과를 slack에 전송
  • 테스트 끝난 후 생성된 자원들을 정리 - ex) ec2 중지 등


대략 이렇습니다.


깃헙액션은 뭔가 대단한 일을 하는 것 처럼 애니메이션을 극적으로 연출해줘서 참 좋습니다. 천재 해커가 된 기분입니다.

눈물 젖은 깃헙액션을 드셔보셨습니까... 저는 먹어보았습니다..




버튼 클릭 하나로 테스트가 자동 생성되어 열심히 돌아가고 있을 때는 정말 기뻤습..니...다...!




성능테스트 결과가 날아온다

json 을 그대로 전송하는 것이라서 예뻐보이지는 않네요.

꾸미기는 사치...

 

 

 

자동으로 중지된 ec2 인스턴스들

요금 비싸니까 바로바로 끄란 말이다 깃헙액션



이제 Java/Spring 코드를 작성하고 수동으로 빌드,배포, 환경세팅, 부하테스트, 결과 저장 등을 진행하지 않아도 됩니다.
버튼 하나만 누르면 알아서 슬랙으로 테스트 결과를 보내주기 때문입지요.

극한의 자동화라고 호들갑 떨어서 죄송합니다. 그냥 이런 것도 된다고 기록해두고 싶었습니다.
비록 글을 쓰다 ec2 인스턴스를 모두 terminate 시키는 바람에 눈물젖은 키보드와 찐한 데이트를 한 번 더 해야 했지만, 도움이 될 누군가와 미래에도 똑같이 같은 문제에 봉착할 제게 이 글을 바칩니다..