API Gateway+lambda를 이용한 파이프라인을 자동화

Glen·2023년 10월 12일
0

원인

  • 인원은 적고 업무가 많아지면서 처리해야 할 업무량이 많아짐
  • 기존 배포 파이프라인에서 CI, CD에서 사람이 확인하고 입력하는 단계가 있음
  • 각 단계별 알림을 확인하지 못할경우 배포가 늦어짐

기존 파이프라인

  • step1. 티켓 발생하면 확인하여 담당자 지정
  • step2. 해당 repository 확인 및 최신 tag값 확인 후 +1 하여 push
  • step3. circleci build 완료를 확인
  • step4. spinnaker의 프로젝트를 찾아 parameter에 tag값 추가.

목적

  • 사람이 확인하는 단계를 최소화 하여 배포를 하자
  • 기존 파이프라인을 수정하지 않고 사용하자

구성도

  1. jira 티켓에서 담당자가 할당되면 automation이 동작하여 api gateway를 webhook으로 호출

    값이 올바르다면 lambda가 동작하여 최신 tag push, notification

  2. tag가 push 되면 circleci start

  3. circleci job complete가 되면 webhook기능으로 api gateway 호출

    bitbucket에서 commit 값으로 tag를 찾는 api는 제공하지 않음.
    tag 검색 api로는 commit 값이 확인됨.

  4. circleci webhook으로 commit을 비교하여 tag를 찾고 spinnaker api 호출

  5. spinnaker api 호출 후 slack으로 noti

  6. spinnaker에 설정해둔 Automated Triggers가 동작됨

  • 처음에는 lambda 대신 ec2에 go rest api로 하려고 했었다.
    - bitbucket api로 원하는 tag값을 검색하는게 어려웠었음.
    - 그런데 ec2에 git clone 하는게 보안상 안좋을것 같았다.

    • 모든 소스코드를 저장해둬야 하는점. 혹시나 탈취되면 큰일남.
  • 결국 api로 돌아가서 사용 방법을 삽질끝에 찾음

  • bitbucket api의 q변수를 사용하면 특정 문자로 시작하는 값을 가져올 수 있었음.

Automation

  • 티켓 내용에 따라 조건들을 설정한다.

  • 티켓에서 필요한 값을 가져다가 lambda와 연결된 api gateway를 호출한다.

  • 이때 보안강화를 위해 api gateway에 ip 접근제한을 추가한다.

    • bitbucket api의 ip whitelist를 참고
    • rest api에는 ip allow, deny 기능이 있으나, http api에는 없기 때문에 lambda 코드에 client ip를 검사하는 코드를 추가했다.
  • api gateway 참고

    • http api는 호출하면 post의 내용에 내가 보내지 않은 여러 클라이언트 값들이 존재한다.
    • rest api는 호출하면 post의 내용에 내가 보낸값만 보낸다.

Automation to lambda

  1. automation에서 보낸 값 추출
    repo, commit
  2. q변수로 stg로 시작하는 태그관련 정보를 받아옴
  3. stg로 시작하는 값만 추출
  4. major, minor, patch 로 숫자를 split하고 patch 값을 +1, 99면 minor +1 / patch 0
  5. 최신 태그 값 push
  6. push가 정상이면 slack으로 noti

아래는 생략된 누추한 코드...

def is_ip_allowed(source_ip):
    # source_ip를 IPv4Address 객체로 파싱
    ip = ipaddress.IPv4Address(source_ip)
    
    # 허용된 CIDR 범위와 비교
    for cidr in allowed_ips:
        network = ipaddress.IPv4Network(cidr, strict=False)
        if ip in network:
            return True
    
    return False

def increment_patch(version):
    major, minor, patch = map(int, version.split("."))
    
    # 패치 값이 99보다  경우 마이너 값을 +1 증가하고 패치는 0으로 설정
    if patch > 99:
        minor += 1
        patch = 0
    else:
        patch += 1
    
    return f"{major}.{minor}.{patch}"

# major.minor.patch값을 sort하기 위해 int로 변환하여 저장
def parse_version(version_str):
    return tuple(map(int, version_str.split('.')))
 
def lambda_handler(event, context):
    request_body = json.loads(event["body"])
    request_ip = event['requestContext']["http"]["sourceIp"]
    if not is_ip_allowed(request_ip):
        # 허용되지 않은 IP 주소 확인
        error_message = "Access denied. Your IP address is not allowed."
        return {
            "statusCode": 403,
            "body": error_message
        }
    
    # 요청 본문에서 <REPO> <commit> 값을 추출
    repo = request_body.get("repo")
    commit = request_body.get("commit")
    
    #repo, commit 값이 없으면 실패
    if not commit or not your_repo:
        return {
            "statusCode": 403,
            "body": "post value is empty."
        }

    # Bitbucket API 엔드포인트 및 헤더 설정
    url = f'https://api.bitbucket.org/2.0/repositories/{proj}/{repo}/refs?q=name~"stg"&sort=-target.date'
    commit_url=f'https://api.bitbucket.org/2.0/repositories/{proj}/{repo}/commit/{commit}'
    push_url = f'https://api.bitbucket.org/2.0/repositories/{proj}/{repo}/refs/tags'
    headers = {
        "Authorization": f"Basic {token}"
    }
    
    commit_response=requests.get(commit_url, headers=headers)
    
    # commit을 확인하고 없으면 실패
    if commit_response.status_code == 404:
        return {
            "statusCode": 404,
            "body": f'no search commit: {commit}'
        }

    
    # Bitbucket API에 GET 요청 보내기
    response = requests.get(url, headers=headers)

    # API 응답이 성공적인지 확인
    if response.status_code == 200:
        # API 응답을 JSON으로 파싱
        data = response.json()
        # stg로 시작하는 값을 추출하여 정렬
        stg_refs = [ref["name"] for ref in data["values"] if ref["name"].startswith("stg")]
        tag_list = [item.replace('stg', '') for item in stg_v_refs]
        
        sorted_versions = sorted(tag_list, key=parse_version, reverse=True)
        print(sorted_versions)
        
        if sorted_versions:
            largest_ref = "v"+sorted_versions[0]
            print("마지막 tag는 stg", sorted_versions[0])
            
            # 메이저, 마이너, 패치 값을 추출하여 패치 값을 +1 증가
            version_numbers = largest_ref.split("v")[1]
            incremented_patch = increment_patch(version_numbers)
            
            # POST 요청을 보내기 위한 데이터 구성
            post_data = {
                "name": f"stg{incremented_patch}",
                "target": {
                    "hash": f"{commit}"
                }
            }
            
            # POST 요청 보내기
            response = requests.post(push_url, headers=headers, json=post_data)    

CircleCI

  • 각 프로젝트별 setting의 기능중 webhook을 사용하려고 생각했으나, ui에서 원하는 데이터를 커스텀 할 수 가 없음.
  • 그래서 circleci의 yaml의 빌드 완료 후 마지막 단계에서 spinnaker에 webhook을 요청하는 구간을 추가 했었다.
    steps:
      - run: 
          name: "webhook spinnaker"
          command: |
            curl --location --request POST "https://api.spinnaker.com/webhooks/webhook/test" \
            --header 'Content-Type: application/json' \
            --data-raw '{
                          "parameters": {
                            "tag": $CIRCLE_TAG
                          }
                        }'
  • 그러나… 프로젝트마다 yaml을 수정해줘야 하는데 프로젝트 갯수가 너무많아 공수가 많이 들게 생겼다...
  • 그래서 결국 다시 돌아가 Project의 Webhook 기능을 이용하여 lambda 호출해서 post값 파싱해서 spinnaker 호출하는 식으로 변경...
    - 이 부분은 삽질이 필요함. 어떤데이터를 보내주는지 부터 확인해야됨.

CircleCI to lambda

  • circleci project webhook에는 두가지 옵션이 존재한다.
    - workflow, job

  • 둘중 하나만(job) 체크해서 진행함.

  • post값을 확인해보니 tag 값을 보내주지 않음.... ㅠㅠㅠㅠ

  • 그리고 bitbucket api에서 원하는 commit값으로 tag값을 가져올 수 없음..

  • 결국 앞에서 작성했던 labmda 코드를 가져다가 응용

    • 최신 tag는 검색이 될테니 최신 tag 10개 가져옴
    • tag가져올때 commit 값도 같이 리스트에 저장함
    • 리스트랑 tag랑 인덱스를 맞춰놓은 상태에서 circleci webhook에서 보내준 commit으로 tag 값을 찾는다.

아래는 생략된 누추한 코드

def find_commit_index(job_commit, check_commit):
    for index, commit in enumerate(check_commit):
        if commit.startswith(job_commit):
            return index
    return -1

# circleci에서 빌드완료환 tag값 찾기
# build 성공 실패 확인 필요
def tag_4_commit(event):    
    request_body=json.loads(event["body"])
    #status확인
    job_status=request_body["job"]["status"]
    #repo name
    repo=request_body["project"]["name"]
    #circleci job name
    job_name=request_body["job"]["name"]
    #job commit
    job_commit=request_body["pipeline"]["vcs"]["revision"]
	
    url = f'https://api.bitbucket.org/2.0/repositories/{proj}/{repo}/refs?q=name~"stg"&sort=-target.date'
    headers = {
        "Authorization": f"Basic {token}"
    }
    #tag값 호출
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
    	data = response.json()
        
        #tag값을 추출하여 저장
        check_tag = [ref["name"] for ref in data["values"] if ref["name"].startswith("stg")]
        
        #commit값을 추출하여 저장
        check_commit = [abc["target"]['hash'] for abc in data["values"] if abc["name"].startswith("stg")]
        
        #commit과 같은 인덱스에 있는 tag 인덱스 값 추출
        index = find_commit_index(job_commit, check_commit)
        
        if index != -1:
            # job_commit를 찾은 경우
            result = check_tag[index]
            return result, repo
    
def spinnaker_trigger(result):
    tag=result[0]
    repo=result[1]

	url = f"https://api.spinnaker.com/webhooks/webhook/test"
    
    post_data = {
		"parameters": {
            "tag": f"{tag}"
        }
    }
    response = requests.post(url, json=post_data)
    print(response.text)


def lambda_handler(event, context):
    result=tag_4_commit(event)
    print(f"commit search {result}")
    if result!=False:
        res=spinnaker_trigger(result)
    else:
        return f"someting fail : {result}"

Lambda

VPC 연결

  • lambda는 aws에서 동작하며, private repository인 bitbucket을 접근해야한다.
  • bitbucket은 ip whitelist가 등록되어있다.
  • ip whitelist에 등록되어있는 접근가능한 vpc 및 서브넷을 연결.

Layer 추가

  • 라이브러리를 사용하기 위해서는 별도로 다운로드 하여 lambda에서 업로드 해줘야 한다.
  • request 라이브러리 다운로드 pip3 install -t ./python requests
  • 다운로드 받은 파일을 zip으로 압축

  • Lambda > 계층에서 생성하여 위 압축파일 및 런타임을 선택

  • Lambda > 함수 > 계층부분에 앞에서 만든 계층 추가

Spinnaker

  • api의 url은 서브 도메인 첫번째 값 뒤에 '-api' 가 붙음
    ex) deploy-stg.kkk.com > deploy-api-stg.kkk.com

  • configuration에 automatied trigger 설정

    • source의 값은 webhook url 구분값이 됨.
    • 각 프로젝트마다 구분하기 쉽게 설정하는것이 좋음.
  • trigger enabled 체크

  • parameters name이 key 값이 됨.

  • post 보낼때 key 값이 맞아야됨.

배포 자동화 확인

  • jira ticket 담당자 지정시 automation 실행

  • 첫번째 람다(automation to lambda) 마지막에 bitbucket tag push 성공 noti

  • 두번째 람다(circleci to lamda) 마지막에 ci (build) complete noti

  • spinnaker webhook trigger 확인

결과

  • 배포속도가 체감상 많이 빨라짐
    • 티켓의 완료 시간으로 확인해보려고 했는데 티켓설정이 맞지 않아서 확인불가...ㅠㅠ
  • 다른거 신경 안쓰고 슬랙으로 배포 실패 성공 확인이 가능
  • 개발자 및 팀원들이 좋아해서 다행

아쉬운점

  • 배포는 태그 기반으로 진행하는데, 운영환경까지 자동화 적용이 애매해짐.
    • 태그의 형태가 qa,production 에서는 일정하지 않음.
    • 만약 적용한다면 기존 태그를 제거하고 규칙을 일정하게 변경해야 할듯...ㅠㅠ
profile
어제보다 나은 엔지니어가 되기 위해서 공부중

0개의 댓글