본문 바로가기

MySQL

Aurora Version & Status 터미널 대시보드

조만간 Aurora MySQL 마이너 버전 업그레이드를 앞두고 있어서 Aurora의 버전과 상태를 확인할 수 있는 터미널 대시보드를 만들었다. DB팀의 작업이 완료되어야 다음 팀에서 작업을 이어받아 진행할 수 있기에 지속적으로 DB 서버의 상태를 모니터링 할 수 있는 대시보드가 필요했다.
실시간으로 Aurora 버전 체크할 때 유용하게 사용할 수 있을 것이다. (PostgreSQL 포함)

godtechwak/aurora_upgrade_check/AuroraUpgradeCheck.go

package main

import (
    "fmt"
    "log"
    "time"
    "os"
    "strconv"
    "text/tabwriter"
    "io/ioutil"
    "strings"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/rds"
  _ "github.com/go-sql-driver/mysql"

)

func readInput(stage string) string {
    for {
        if stage == "region" {
            fmt.Printf("Region(kr/jp/ca/uk): \n")
            var region string
            _, err_region := fmt.Scan(&region)

            if err_region != nil {
                fmt.Printf("Input Data Error: %v\n", err_region)
                continue
            }

            if region == "kr" || region == "jp" || region == "ca" || region == "uk" {
                return region
            } else {
                fmt.Printf("Input correct Region(%s)\n", region)
                continue
            }
        } else if stage == "worktype" {
            fmt.Printf("WorkType(cluster/instance): \n")
            var worktype string
            _, err_worktype := fmt.Scan(&worktype)

            if err_worktype != nil {
                fmt.Printf("Input Data Error: %v\n", err_worktype)
                continue
            }

            if worktype == "cluster" || worktype == "instance" {
                return worktype
            } else {
                fmt.Printf("Input correct WorkType(%s)\n", worktype)
                continue
            }
        }
    }
}

func auroraVersionParamCluster(svc *rds.RDS, lines []string, w *tabwriter.Writer, num int64) {
    init_map := make(map[string]string) // 수행시간을 담기 위한 맵
    var count int //반복횟수
    count = 1
    var duration time.Duration //수행시간


    for {
        fmt.Fprintf(w, "──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t\n")
        fmt.Fprintf(w, " Time│\t Duration|\t Cluster│\t Version│\t Status│\t Param Status│\t\n")
        fmt.Fprintf(w, "──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t\n")
        for _, rcluster_name := range lines {
            currentTime := time.Now()
            hour, min, sec := currentTime.Clock()
            millisec := currentTime.Nanosecond() / 1000000

            timeFormat := "15:04:05.000"
            timeString := fmt.Sprintf("%02d:%02d:%02d.%03d", hour, min, sec, millisec)

            if count == 1 {
              init_map[rcluster_name] = timeString //최초 수행된 시간을 맵에 담아놓는다.
            } else {
              first_timeString, exists := init_map[rcluster_name]

              if exists {
                first_time, err_firsttime := time.Parse(timeFormat, first_timeString)
                after_time, err_aftertime := time.Parse(timeFormat, timeString)

                if err_firsttime != nil || err_aftertime != nil {
                  fmt.Printf("Parsing Error: %s %s", err_firsttime, err_aftertime)
                  return
                }

                duration = after_time.Sub(first_time) //마지막에 수행된 시간에서 최초 수행된 시간의 차를 계산한다.
              }
            }

            input := &rds.DescribeDBClustersInput{
                    DBClusterIdentifier: aws.String(rcluster_name),
                }

            result, err := svc.DescribeDBClusters(input)

            if err != nil {
                fmt.Println("DBCluster Error: ", err)
                return
            }

            // 클러스터 기본 정보(클러스터명, 엔진 버전, 클러스터 상태)
            cluster_info := result.DBClusters[0]
            // DB 클러스터 파라미터 그룹 상태
            cluster_param := result.DBClusters[0].DBClusterMembers[0]

            // DB 클러스터 및 클러스터 파라미터 정보 출력
            fmt.Fprintf(w, "%s│\t %s|\t %s│\t %s│\t %s│\t %s│\t\n", timeString, duration, *cluster_info.DBClusterIdentifier, *cluster_info.EngineVersion, *cluster_info.Status, *cluster_param.DBClusterParameterGroupStatus)

        }

        fmt.Fprintf(w, "──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t\n")
        fmt.Print("\033[2J\033[H")
        w.Flush()
        time.Sleep(time.Duration(num) * time.Millisecond)
        count += 1
    }
}

func auroraVersionParamInstance(svc *rds.RDS, lines []string, w *tabwriter.Writer, num int64) {
  init_map := make(map[string]string) // 수행시간을 담기 위한 맵
    var count int //반복횟수
    count = 1
    var duration time.Duration //수행시간

    for {
        fmt.Fprintf(w, "──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t\n")
        fmt.Fprintf(w, " Time│\t Duration│\t Instance│\t Version│\t Status│\t Param Status│\t\n")
        fmt.Fprintf(w, "──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t\n")

        for _, rinstance_name := range lines {
            currentTime := time.Now()
            hour, min, sec := currentTime.Clock()
            millisec := currentTime.Nanosecond() / 1000000

            timeFormat := "15:04:05.000"
            timeString := fmt.Sprintf("%02d:%02d:%02d.%03d", hour, min, sec, millisec)

            if count == 1 {
              init_map[rinstance_name] = timeString //최초 수행된 시간을 맵에 담아놓는다.
          } else {
              first_timeString, exists := init_map[rinstance_name]

              if exists {
                first_time, err_firsttime := time.Parse(timeFormat, first_timeString)
                after_time, err_aftertime := time.Parse(timeFormat, timeString)

                if err_firsttime != nil || err_aftertime != nil {
                  fmt.Printf("Parsing Error: %s %s", err_firsttime, err_aftertime)
                  return
                }

                duration = after_time.Sub(first_time) //마지막에 수행된 시간에서 최초 수행된 시간의 차를 계산한다.
              }
            }

            input := &rds.DescribeDBInstancesInput{
                DBInstanceIdentifier: aws.String(rinstance_name),
            }

            result, err := svc.DescribeDBInstances(input)

            if err != nil {
                fmt.Println("Error", err)
                return
            }

            // 인스턴스 기본 정보(인스턴스명, 엔진 버전, 인스턴스 상태)
            instance_info := result.DBInstances[0]
            // DB 인스턴스 파라미터 그룹 상태
            instance_param := result.DBInstances[0].DBParameterGroups[0]

            // DB 인스턴스 및 인스턴스 파라미터 정보 출력
            fmt.Fprintf(w, "%s│\t %s│\t %s│\t %s│\t %s│\t %s│\t\n", timeString, duration, *instance_info.DBInstanceIdentifier, *instance_info.EngineVersion, *instance_info.DBInstanceStatus, *instance_param.ParameterApplyStatus)
        }

        fmt.Fprintf(w, "──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t ──────────────────────────────────\t\n")
        fmt.Print("\033[2J\033[H")
        w.Flush()
        time.Sleep(time.Duration(num) * time.Millisecond)
        count += 1
    }
}

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Please provide a number as argument.")
        return
    }

    num, err := strconv.ParseInt(os.Args[1], 10, 64)
    if num < 100 {
        fmt.Println("Input more than 100 milliseconds")
        return
    }

    if err != nil {
        fmt.Printf("Error parsing number: %v", err)
        return
    }

    var real_region string

    w := tabwriter.NewWriter(os.Stdout, 0, 0, 0, ' ', tabwriter.AlignRight)

    fmt.Printf("================================\n")
    fmt.Printf("Aurora Version & Parameter Check\n")
    fmt.Printf("================================\n")

    region := readInput("region")
    worktype := readInput("worktype")

    regionMap := map[string]string {
        "kr": "ap-northeast-2",
        "jp": "ap-northeast-1",
        "ca": "ca-central-1",
        "uk": "eu-west-2",
    }

    real_region, ok := regionMap[region]

    if !ok {
        fmt.Println("Invalid region provided")
        return
    }

    sess, err := session.NewSessionWithOptions(session.Options{
        SharedConfigState: session.SharedConfigEnable,
        Config: aws.Config{
            Region: aws.String(real_region),
        },
    })

    if worktype == "cluster" {
        filePath := "./db_cluster_list_" + region + ".txt"
        content, err := ioutil.ReadFile(filePath)

        if err != nil {
            log.Fatal(err)
        }
        cluster_name := string(content)
        lines := strings.Split(cluster_name, "\n")

        svc := rds.New(sess)

        auroraVersionParamCluster(svc, lines, w, num)

    } else if worktype == "instance" {
        filePath := "./db_instance_list_" + region + ".txt"
        content, err := ioutil.ReadFile(filePath)

        if err != nil {
            log.Fatal(err)
        }

        instance_name := string(content)
        lines := strings.Split(instance_name, "\n")

        svc := rds.New(sess)

        auroraVersionParamInstance(svc, lines, w, num)
    }
}

 

리전을 입력하고, 클러스터 또는 인스턴스를 선택하여 DB 서버의 버전 및 상태를 모니터링 한다.

 

 

버전과 상태 정보를 클러스터별로 반복적으로 가져오기 때문에 장비 대수가 많을 경우 몇 십초 가량이 소요될 수 있다. 고루틴을 고려해보았으나 클러스터의 순서가 뒤바뀔 수 있고, 그렇다고 뮤텍스를 통해 순서를 제어하기에는 오버 엔지니어링인 것 같아 요 정도에서 마무리하였다.

아래 스샷 기준으로는 36대의 클러스터 정보를 대시보드에 구성할 때 약 9~10초 정도(리프레시 타임)가 소요되었다. 한국 리전 기준이기 때문에 글로벌 서비스일 경우에는 네트워크 레이턴시를 고려해야 한다. 대신 글로벌 리전은 RTT(Round Trip Time in networking) 때문에 API 호출 횟수에 영향을 받지 않는데, 한국이나 한국과 가까운 일본 리전의 경우 API 호출 횟수에 쓰로틀링 제한이 발생할지도 모르니 time sleep을 적정하게 설정해주어야 한다.

리스트업된 클러스터 수를 프로세스 단위로 분할하여 여러 개의 프로세스를 띄우는 방법도 괜찮을 듯 하다.