문제

Actions에서 도커 이미지를 빌드하여 AWS의 ECR에 업로드 하기위해 하던 작업에는

기존에 AWS의 Access key와 secret을 github secrets에 저장해두고 사용하던 방식이 있지요.

해당 액션은 아래처럼 github secret에 IAM에서 생성한 유저의 액세스키를 등록하여 사용하였지요.

...
    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v2
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-2

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v2

    - name: Build, tag, and push the image to Amazon ECR
      run: |
        TAG=$(git rev-parse --short HEAD)
        echo "TAG=${TAG}" >> $GITHUB_ENV
...

AWS Console에서는 해당 액세스 키를 위해 별도의 유저를 만들고 해당 유저의 역할에 맞는 권한만 부여해서 사용했지요.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "ecr:InitiateLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:CompleteLayerUpload",
                "ecr:PutImage"
            ],
            "Resource": "*"
        }
    ]
}

기존 방식대로 해도 문제는 되지 않지요, 하지만 관리적인 요소가 필요할뿐

유출되었을때의 문제점, 그리고 revoke이후의 재등록 과정이 귀찮을 수 있지요.

방향

이걸 해결 하기 위한 방법이 OIDC(링크)를 활용한 방법이지요.

자격 증명이 긴 시간의 매체 대신, 그때그때 짧은 시간동안만(설정에따라 기본값 3600초) 유효한 자격을 얻어 AWS에 액세스 하는 방식이지요.

그럼 어떻게 하는지 한번 진행해 봅시다.

진행

해당 내용은 위 링크에도 잘 나와있지만, 익숙하지 않다면, 조금 불편할수도 있기에 아래 이미지를 잘 보시지요.

IAM > Access management > Identity providers로 이동하여 Add provider를 클릭

 

 

그리고 아래와 같이 입력을 후

Get thumbprint 클릭

Add provider 클릭 -> 저장

 

Thumbprint는?

이는 SSL/TLS 인증성의 지문 같은거라고 보면 되지요, AWS에서는 이 값을 사용해서 응답에 대한 신뢰성을 검증 하지요.

이 값이 일치하지 않으면 요청은 거절되지요.

이걸 구하는 방법은 openssl로 해서 aws-cli로 등록하는 방식도 있는데, 복잡해서, 이렇게 클릭 몇번으로 해결하지요.

 

 

IAM > Policies > Create policy 클릭

기존에 액션에서 사용하던 사용자의 권한정책을 그대로 가져오지요.

그리고 Next를 눌러 적당한 정책 이름(github-action-policy)을 넣어주고 생성하지요

 

AWS에서는 마지막으로 IAM > Roles > Create role 클릭

Web Identity를 선택

공급자를 아까 등록했던 공급자로 지정하지요.

조직과 레포, 브랜치는 해당 조직의 모든걸 허용한다는 전제로 아래처럼

조직 or 개인계정을 써주고

이하 *을 작성합니다.

그리고 Next

아까 미리 만들어준 정책을 주입하고 Next

 

Role name을 적당히 작성해주고

신뢰요소를 확인하지요.

자 여기서 신뢰요소는 수정할 필요가 있지요!

조건이 StringEquals가 아니라, StringLike로 해야하지요

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::xxxxxxxxx:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": [
                        "repo:org or github id/*:*"
                    ]
                },
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": [
                        "sts.amazonaws.com"
                    ]
                }
            }
        }
    ]
}

 

그럼 이제 AWS 에서 할 작업은 끝났지요.

이제 Actions를 수정할 차례 이지요.

 

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v2
      with:
        role-to-assume: arn:aws:iam::xxxxxxxx:role/github-action-role
        aws-region: ap-northeast-2

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v2

    # - name: Configure AWS credentials
    #   uses: aws-actions/configure-aws-credentials@v2
    #   with:
    #     aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    #     aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    #     aws-region: ap-northeast-2
        
    # - name: Login to Amazon ECR
    #   id: login-ecr
    #   uses: aws-actions/amazon-ecr-login@v2

 

그리고 액션을 실행하면

끝! 이지요

 

 

주의할점

fork 된 레포에서는 작동하지 않지요.

그리고 이글(링크)에서 보면 나오듯이 포크에 대한 악용 문제도 나오니 참고하세요.

role-to-assume에 할당하는 arn을 시크릿에 보관하면 해결가능!

알면서 귀찮아서 안했던 작업

 

AWS쓰면 CodeDeploy 통해서 그나마 쉽게 할 수 있을것 같은데,

젠킨스 써도 쉽게 할 수 있겠지?

 

젠킨스에서 쉽게 하는 방법 저는 잘 몰라용.

그냥 서버 여러군데 배포할때 한방에 하는 방법

여러개 돌아가며 누르는것도 괜찮지만, 그것마저 귀찮은게 사람이니까.

특히나, L4 스위칭까지 하려면 여간 귀찮은게.

 

물론 git에서 웹훅 날려서 하는 방법도 있긴한데, 막 모델이나 데이터 같은거 변경되었을때 이러면 위험하니까

클릭으로 하는 방법!

요거 하나 누르면 땡!

 

일단 현재 회사에서는 L4에서 특정 REST API의 상태값을 통해 연결할지 말지 여부를 결정짓는다.

그래서 해당 상태값을 "점검"으로 바꿔주면 해당 WAS로 연결은 끊기고 배포가 가능하다는 점.

 

젠킨스에서 보통 프로젝트 만들어서 하는 것과 과정은 유사하다.

  1. Git Clone
  2. 빌드
  3. 배포 및 실행

여러 WAS에 배포하기 위해 상태를 확인하고, 변경하는 작업이 추가로 들어가면 될것 같다.

 

젠킨스에서 제공해주는 파이프라인 스크립트는 두가지 방식으로 작성 할 수 있다.

  • 선언적 파이프라인 => 여기선 이걸로 진행한다. pipeline { } 이 감싸고 있음.
  • 스크립티드 파이프라인(노드) => 얘가 뭔가 간단해 보이는데, 담에 시간날때 시도해 봐야지, 다음 생에...

 

New Item 해서 Pipeline 선택하고 다음으로 넘어가면! Pipeline 란 하단에 Pipeline Syntax 라는 링크가 있다. 요 녀석을 눌러서 페이지에 가보면 파이프라인 스크립트 작성에 도움을 주는 툴이 있는걸 확인 할 수 있다. 제네레이터.

 

기본적은 스크립트는 위 처럼 자동으로 생성 할 수 있다.

  • agent
    • 스크립트를 실행할 에이전트를 지정하는데, 라벨로 지정이 가능하며, any로 하면 그냥 됨.
  • 구조
    • stages > stage > steps 이며, 별도로 jenkins에서 제공해주는, environment, tools 등을 사용 할 수 있다.
    • 이 부분은 Declarative Directive Generator 에서 생성 가능하다.
    • tools 같은게, 선언적 파이프라인과 스크립티드 파이프라인에서 쓰는 작성 법이 다르더라.
    • 각 스테이지의 이름은 알아보기 쉽게 작성하는게 좋긴하다.
    • Git Clone과 Publish on SSH에서 사용하는 Credential 관련된 정보는 사전에 등록해두면 제네레이터에서 사용 가능하다.
  • Git Clone 작성

제네레이터를 이용해 작성해 보자, 빈칸만 채우면 된다.

  • 빌드
    • 위와 마찬가지로 제네레이터를 이용해 tools의 Gradle을 사용하는 방법도 있지만, gradlew를 사용하면 더 편하다.
    • 보통 젠킨스가 설치된 서버 특성상 자바 버전도 여러개 일거고, 그런거 선택 자체를 gradlew 안에 넣어두면 더 용이하다.
  • 체크 및 스위칭 
    • 이건 CURL을 활용해서 하면 된다.
    • 결국 쉘 스크립트를 실행하는건데, 제네레이터의 script를 써서 쉘을 실행 할 수 있다.(groovy 문법이라고 한다. 얼핏 본거 같은데...)
    • 추가적으로 curl을 통해 얻어오는 Json값을 어떻게 가져올까 하고 grep 명령어 옵션을 엄청 찾았는데, jq 라는 간단한 커맨드 처리자가 있다. 대부분의 OS를 지원하고, 사용법도 간단하다. https://stedolan.github.io/jq/
script{         
    TOKEN = sh (
        script: '''
            curl -X POST "https://domain/user/sign-in" -H "Content-Type: application/json" -d '{"email":"email@account.com", "password":"xxxxxxxx", "remember": "false"}' | jq '.response.accessToken'
        ''',
        returnStdout: true
    ).trim()
}
  • 배포
    • 배포는 젠킨스의 publish on ssh를 사용했다.
    • 물론, 이것도 제네레이터를 사용해 작성 가능하다.
sshPublisher(
    publishers: 
        [
            sshPublisherDesc(
                configName: '설정된 SSH 접속 대상 서버 이름', 
                transfers: 
                    [
                        sshTransfer(
                            cleanRemote: false, 
                            excludes: '', 
                            execCommand: '''
                                            bash deploy.sh // 실행할 쉘 내용?
                                        ''', 
                            execTimeout: 120000, 
                            flatten: false, 
                            makeEmptyDirs: false, 
                            noDefaultExcludes: false, 
                            patternSeparator: '[, ]+', 
                            remoteDirectory: '파일을 업로드 할 폴더', 
                            remoteDirectorySDF: false, 
                            removePrefix: 'build/libs', // 제거할 프리픽스
                            sourceFiles: 'build/libs/result.jar' // 업로드할 파일
                        )
                    ], 
                usePromotionTimestamp: false, 
                useWorkspaceInPromotion: false, 
                verbose: true // 과정을 로그로 출력한다.
                )
        ]
    )

처음 해보면 어려울 수도 있겠지만, 단계적 절차를 밟아가며 수정 보완하며 작성하다보면 결국 잘 되기 마련이다.

이러한 파이프라인 코드를 Git에 올려두고 수정 & 사용 하는 것도 가능하다.

 

  • 작성 결과
pipeline {
    agent any

    stages {
        
        stage('git clone') {
            steps {
                git branch: '${branch}', credentialsId: '${credentialsId}', url: '${git_url}'
            }
        }
        
        stage('build') {
            steps{
                sh '''
                    echo build start
                    ./gradlew clean build
                    echo build end
                '''
            }
        }
        
        stage('get Token') {
            steps {
                script{
                    
                    TOKEN = sh (
                        script: '''
                            curl -X POST "https://domain/user/sign-in" -H "Content-Type: application/json" -d '{"email":"email@domain.com", "password":"xxxxxxxx", "remember": "false"}' | jq '.response.accessToken'
                        ''',
                        returnStdout: true
                    ).trim()
                    
                }
            }
        }
        
        stage('check status server1') {
            steps {
                script{ 
                    HTTP_CODE_GET = "200"
                    while(HTTP_CODE_GET == "200"){
                        
                        HTTP_CODE_POST = sh (
                            script: """
                                curl -X PATCH "http://server1ip/config/loadBalance/server1ip" -H "Content-Type: application/json" -H "authorization: Bearer $TOKEN" -o /dev/null -w "%{http_code}"
                            """,
                            returnStdout: true
                        ).trim()
                        
                        sleep 5
                        
                        HTTP_CODE_GET = sh (
                            script: '''
                                curl -o /dev/null -w "%{http_code}" "http://server1ip/config/isMaintenance"
                            ''',
                            returnStdout: true
                        ).trim()
                        
                        echo "${HTTP_CODE_GET} while"
                    }
                    
                    sleep 45
                        
                }
                echo "${HTTP_CODE_GET} break"
            }
        }
        
        stage('publish on ssh for server-1') {
            steps {
                sshPublisher(
                    publishers: 
                        [
                            sshPublisherDesc(
                                configName: '${Server1}', 
                                transfers: 
                                    [
                                        sshTransfer(
                                            cleanRemote: false, 
                                            excludes: '', 
                                            execCommand: '''
                                                            bash deploy.sh;
                                                        ''', 
                                            execTimeout: 120000, 
                                            flatten: false, 
                                            makeEmptyDirs: false, 
                                            noDefaultExcludes: false, 
                                            patternSeparator: '[, ]+', 
                                            remoteDirectory: '${remoteDir}', 
                                            remoteDirectorySDF: false, 
                                            removePrefix: 'build/libs', 
                                            sourceFiles: 'build/libs/deploy.jar'
                                        )
                                    ], 
                                usePromotionTimestamp: false, 
                                useWorkspaceInPromotion: false, 
                                verbose: true
                                )
                        ]
                    )
            }
        }
        
        stage('resotre status server1') {
            steps {
                script{ 
                    sleep 60
                    
                    while(HTTP_CODE_GET != "200"){
                        
                        HTTP_CODE_POST = sh (
                            script: """
                                curl -X PATCH "http://server1ip/config/loadBalance/server1ip" -H "Content-Type: application/json" -H "authorization: Bearer $TOKEN" -o /dev/null -w "%{http_code}"
                            """,
                            returnStdout: true
                        ).trim()
                        
                        sleep 5
                        
                        HTTP_CODE_GET = sh (
                            script: '''
                                curl -o /dev/null -w "%{http_code}" "http://server1ip/config/isMaintenance"
                            ''',
                            returnStdout: true
                        ).trim()
                        
                        echo "${HTTP_CODE_GET} while"
                    }
                    
                    
                }
            }
        }
        
        
        stage('check status server2') {
            steps {
                script{ 
                    HTTP_CODE_GET = "200"
                    while(HTTP_CODE_GET == "200"){
                        
                        HTTP_CODE_POST = sh (
                            script: """
                                curl -X PATCH "http://server2ip/config/loadBalance/server2ip" -H "Content-Type: application/json" -H "authorization: Bearer $TOKEN" -o /dev/null -w "%{http_code}"
                            """,
                            returnStdout: true
                        ).trim()
                        
                        sleep 5
                        
                        HTTP_CODE_GET = sh (
                            script: '''
                                curl -o /dev/null -w "%{http_code}" "http://server2ip/config/isMaintenance"
                            ''',
                            returnStdout: true
                        ).trim()
                        
                        echo "${HTTP_CODE_GET} while"
                    }
                    
                    sleep 45
                        
                }
                echo "${HTTP_CODE_GET} break"
            }
        }
        
        stage('publish on ssh for api-2') {
            steps {
                sshPublisher(
                    publishers: 
                        [
                            sshPublisherDesc(
                                configName: 'server2', 
                                transfers: 
                                    [
                                        sshTransfer(
                                            cleanRemote: false, 
                                            excludes: '', 
                                            execCommand: '''
                                                bash deploy.sh;
                                                ''', 
                                            execTimeout: 120000, 
                                            flatten: false, 
                                            makeEmptyDirs: false, 
                                            noDefaultExcludes: false, 
                                            patternSeparator: '[, ]+', 
                                            remoteDirectory: '${remoteDir}', 
                                            remoteDirectorySDF: false, 
                                            removePrefix: 'build/libs', 
                                            sourceFiles: 'build/libs/deploy.jar'
                                            )
                                    ], 
                                usePromotionTimestamp: false, 
                                useWorkspaceInPromotion: false, 
                                verbose: true
                            )
                        ]
                )
            }
        }
        
        stage('resotre status server2') {
            steps {
                script{ 
                    sleep 60
                    
                    while(HTTP_CODE_GET != "200"){
                        
                        HTTP_CODE_POST = sh (
                            script: """
                                curl -X PATCH "http://server2ip/config/loadBalance/server2ip" -H "Content-Type: application/json" -H "authorization: Bearer $TOKEN" -o /dev/null -w "%{http_code}"
                            """,
                            returnStdout: true
                        ).trim()
                        
                        sleep 5
                        
                        HTTP_CODE_GET = sh (
                            script: '''
                                curl -o /dev/null -w "%{http_code}" "http://server2ip/config/isMaintenance"
                            ''',
                            returnStdout: true
                        ).trim()
                        
                        echo "${HTTP_CODE_GET} while"
                    }
                    
                    
                }
            }
        }
    }
}

docker compose

 

version: '3.7'

services:
    mariadb:
        image: mariadb
        ports:
            - "3306:3306"
        volumes:
            - ./db/conf.d:/etc/mysql/conf.d
            - ./db/data:/var/lib/mysql
            - ./db/initdb.d:/docker-entrypoint-initdb.d
        environment:
            MYSQL_HOST: localhost
            MYSQL_ROOT_PASSWORD: rootpw
            MYSQL_ROOT_HOST: '%'
            TZ: Asia/Seoul
        networks:
            - default
        restart: always
        stdin_open: true
        tty: true
    redis:
        image: redis
        command: redis-server --requirepass redispw --port 6379
        container_name: redis6379
        hostname: redis6379
        labels:
            - "name=redis"
            - "mode=standalone"
        ports:
            - "6379:6379"
        networks:
            - default
        volumes:
            - ./redis:/data
        restart: always
        stdin_open: true
        tty: true
    rabbitmq:
        image: 'rabbitmq'
        container_name: rabbitmq-stream
        ports:
            - "5672:5672"
            - "15672:15672"
        networks:
            - default
        volumes:
            - ./rabbitmq/data:/var/lib/rabbitmq
            - ./rabbitmq/log:/var/log/rabbitmq
        environment:
            RABBITMQ_ERLANG_COOKIE: "RabbitMQ-My-Cookies"
            RABBITMQ_DEFAULT_USER: "admin"
            RABBITMQ_DEFAULT_PASS: "adminpw"
        restart: always
        stdin_open: true
        tty: true
    elasticsearch:
        container_name: elasticsearch
        image: elasticsearch:8.0.0
        ports:
            - "9200:9200"
            - "9300:9300"
        networks:
            - default
        volumes:
            - ./elasticsearch:/usr/share/elasticsearch/data
        environment:
            - ELASTIC_USERNAME=elastic
            - ELASTIC_PASSWORD=elasticpw
            - xpack.security.enabled=false
            - xpack.security.transport.ssl.enabled=false
            - http.cors.enabled=true
            - http.cors.allow-origin=*
            - discovery.type=single-node
            - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
            - TZ=Asia/Seoul
        restart: always
        privileged: true
        stdin_open: true
        tty: true
    kibana:
        container_name: kibana
        image: kibana:8.0.0
        ports:
            - "5601:5601"
        networks:
            - default
        environment:
            ELASTICSERCH_UTL: http://elasticsearch:9200
            ELASTICSEARCH_HOSTS: http://elasticsearch:9200
            TZ: Asia/Seoul
        restart: always
        privileged: true
        stdin_open: true
        tty: true

networks:
    default:
        external:
            name: local-env-network
# docker network create local-env-network
# 컨테이너ID 확인
$ docker ps

# 해당 컨테이너 bash 실행
docker exec -it -u root ${CONTAINER ID} /bin/bash

 

본래 Bitbucket의 Id/Pw를 사용해서 연동하려 했으나,

지속적으로 returned status code 128: 를 뱉음.

 

원인을 알 수 없는 와중에 구글링 해보니,

repository 생성시 private으로 생성한 경우 id/pw를 사용한 연결이 불가능 하다고...ㅠ

 

그래서 sshkeygen을 사용해서 쌍키 생성 후 

공개키는 bitbucket에 비밀키는 젠킨스 계정관리에 등록해줌

 

그리고 Git 접속 경로는 https가 아닌 ssh 접속

매번 하는 작업인데 항상 중간부분을 까먹어서 헤매다 보니, 정리를 좀 해두는게 좋겠다 싶어서..

 

톰캣은 일단 tar.gz 파일을 받고,

 

폴더 구조는 app 과 web 으로 구분을 짓는다.

 

app 아래에 여러 톰캣을 넣어놓고, web아래는 webapps를 옮겨서 해당 서비스 이름으로 변경한다.

 

톰캣/conf/server.xml 의 docBase를 확인해보면 webapps로 되어 있을텐데,

이걸 나는 web 아래로 복사해서 서비스명으로 변경하였기 때문에,

 

docBase="/web/서비스명/"으로 해준다.

 

이렇게 구조를 나누는 이유는...전에 SE 한분을 일한적이 있는데 이렇게 하는게 깔끔하다고 해서...ㅎㅎ

 

그리고 ci에서 배포를 하기 위해 tomcat-users.xml 또한 설정 한다.

 

'

설정이 제대로 안되었을 경우 매니저 접속시 이런 페이지를 만날 수 있다.

 

manager-* 각 롤을 마다 역할이 다르며, ci에서 이용하기 위한 롤은 script다.

 

여기까지는 늘상 알고 있고 딱히 헷갈릴것도 없는데...

 

항상 이부분이 문제

딱히 문제랄건 없고, 꼭 이 페이지를 만나야 기억이 난다 ㅠㅠ

 

manager/META-INF/context.xml 을 확인해보면

<Context> 태그 아래에 기본적으로 두개의 밸브가 있다.

이 설정은 외부에서 톰캣 매니저로의 접속을 막는 설정이며,

해당 설정을 주석처리 해주거나, ip를 정확히 지정해주면 외부에서 매니저로 접속이 가능하다.

 

운영환경에서 보통 톰캣과 ci를 같은서버에 두지 않기에, 이정도 설정을 알아놓으면 아주 유용할듯.

 

그럼 오늘도 고생하세유.

+ Recent posts