현재 회사의 프론트 배포 프로세스는 매우 고전스러운(?) 방식으로 진행되고 있었다. 바로! 로컬에서 build하고 build한 파일을 원격서버에 접속해서 복붙하고 재실행시키는 방식인데... 특이한건 윈도우 서버에다가 IIS란걸 쓰고 있었더랬다...
이게 나 혼자 개발을 쭈우우욱 할거면 문제는 안되겠지, 조금 귀찮을뿐.. 그러나 회사 규모는 점점 커질것이고 같이 프론트 개발 할 사람이 들어올 것이다. 그럴땐 어떻게 해야할까? 만약 동시에 같은 build 폴더를 덮어씌우기 한다면? 코드가 통합되지 않은 상태라면? 여러모로 골치아픈 문제들이 생길 것 같아서 여유가 있을때, 서비스 규모가 작을때 배포 자동화 작업을 하려고 한다 나름 그래도 개발, 운영이 나누어져있는데 개발부터 배포 자동화를 셋팅해볼 것이다
조건
- github actions는 플랫폼내 가상 머신에서 실행되는데, 해당 IP는 매번 바뀐다
- AWS EC2 인스턴스로 관리되고 있는 window 서버에 접근할 수 있는 IP는 제한되어야한다
사실 1차 시도에 실패했는데... 우리 서버에 IP 제한이 걸려있는지 모르고 평소 하던 방식대로 github actions를 호로로롤 작업해서 실행해봤었다. 그러다가 새로 알게된 사실! github actions를 실행하는 가상머신의 IP는 매번 바뀐다와 우리 서버 IP 제한이 걸려있던것
이걸 이사님과 상의를 해봤는데 이사님은 서버의 IP 제한을 풀고싶지 않다고 하셨다. 이대로 github actions는 못 쓰고 다른 방법을 알아봐야하나 싶었는데... 역시 방법은 다양했다
시도할 수 있는 방법
- github actions의 IP를 일시적으로 허용한다
- git에서도 권장하지 않는 방법이다 [링크]
- AWS SDK, AWS CLI를 통해서 해당 인스턴스에 접근 후 인바운드 규칙을 동적으로 조작할 수 있는데 이사님이 반대함
- 아무튼 보안적으로 찜찜한 구석이 있다
- 자체 중간 서버를 구축 [링크]
- 자체 중간 서버를 구축하는 방법은 여러가지이지만, 이 또한 관리 포인트가 들어나는 것이라고 생각한다
- 결국 해당 서버도 github actions를 프라이빗하게 소통하도록(?) 구성 해야한다. 그러면 차라리 배포할 서버와 github actions 프라이빗 소통을 다이렉트로 구축하고 말지 굳이 중간 경유지를 만들 필요가 있을까?
- WireGuard [링크]
- 별도의 중간 서버를 구축하지 않고 오버레이 네트워크 즉, 가상 네트워크를 설정하여 통신하도록 설정
- WireGuard를 활용하면 네트워크 오버레이가 안전한 VPN을 통해 암호화된 연결로 구현된다
- 최종적으로 설정할땐 해당 AWS EC2 인스턴스에 WireGuard VPN이 접근할 수 있는 IP를 추가해줘야하긴 한다
- 클라이언트는 해당 WireGuard VPN 네트워크에 연결된 장치들만 EC2에 접근할 수 있도록 제한 설정을 한다
- 클라이언트가 정적 IP 구조로 설정되어 있어야하는데, 다른곳에 작업하면 IP가 바뀌게 된다면?
- 사실 이 방법은 그다지 대중적이지도 않고, 자료가 적었으며 무엇보다 작업자인 내가 시도하기엔 네트워크 지식이 부족해서 하나의 방법을 시도했다가 실패하면 다른 방법을 알 수가 없다는게 문제였다
- OpenID Connect (OIDC) [링크]
- 토큰을 사용하여 github actions 외부에서 워크플로우를 인증하는 방식
- 토큰 발행의 주체는 github acitons에서 실행되는 워크플로우에 의해 github 자체가 발행
- 프라이빗 네트워크의 에지에서는 OIDC 토큰으로 들어오는 요청을 인증한 다음 프라이빗 네트워크에서 워크플로우를 대신하여 API 요청을 하는 API 게이트웨이를 실행한다
- OIDC 토큰은 다른 github actions 사용자가 해당 네트워크에 액세스 할 수 없도록 예상 워크플로우에서 특별히 온 것인지를 확인하기위해 특정 조직의 레파지토리에서 시작된 요청에만 응답하도록 구성해야한다
- 즉, 우리가 로그인 인증 방식때 JWT 토큰을 사용하여 만료기간을 정해두는 것처럼 OIDC도 임시 토큰을 사용하므로 만료기간을 설정할 수 있어서 직접 특정 IP를 허용해서 늘 열어두는 것보단 보안적이다라고 할 수 있다
- JWT 토큰에는 워크플로우 정보(레파지토리 이름, 워크플로우 ID, 브랜치 등등)의 내용이 포함되어있다
- 또한, 원격서버에 직접 접근하는 방식이 아니고 음… AWS 범위 내에서 해당 인스턴스를 조작하는 권한을 부여받아 조작하는 느낌? 그러니 나한테 부여받은 권한만 쓸 수 있으니 보안적이다
- 그러나 우린 해당 원격서버와 직접 통신을 해야하기에 추가적인 설정이 다소 복잡할 수 있다 (IAM 역할과 정책 부분)
- IAM 정책 구성에 익숙하지 않으면 안쓰는게 좋다 그러나 우린… 인프라팀도 있고, 조언을 구할 사람이 있다고 판단해서 시도했으나.. 막상 잘 아는 사람이 없어서 공부해가며 권한을 하나씩 빼는 작업을 해야할 것 같다
프로세스 방향
- 로컬에서 작업 후 develop 브랜치에 작업 내역을 origin push
- github actions 가성 머신에서 작업 실행
- Node.js 설치
- yarn 설치
- 해당 레파지토리에 설정한 env 파일 내용을 로컬에 복사 (파일명 .env.development)
- CI=false yarn build:dev
- 개발 전용 빌드 명령어 실행
- CI-false는 ts 경고문 무시 (이렇게 설정을 안해주면 테스트에 통과하지 못해서 build 실패)
- build 폴더를 zip 파일로 압축
- OpenID Connect를 사용하여 자격증명
- S3에 build.zip 파일 업로드
- AWS Cli를 이용하여 send command 실행
- 지정된 EC2 인스턴스에 접근
- EC2 원격서버에서 build 폴더를 압축 해제해야할 디렉토리로 이동
- 기존에 있는 build 폴더 백업
- S3에 build.zip 파일 다운로드
- 압축 해제
- web.config 복사해서 build 폴더내 복붙
- IIS에 해당 도메인 재실행
OpenID Connect (OIDC) 시작하기
아무튼 WireGuard 보다는 얻을 수 있는 정보가 더 많고 공식 문서도 잘 되어있어서 해당 방식을 선택했다
ID 제공업체 설정 [링크]
AWS 관리 콘솔, AWS 명령줄 인터페이스, Windows PowerShell 도구 또는 IAM API를 사용하여 IAM OIDC ID 공급자를 만들고 관리할 수 있다
즉, 토큰을 발행해줄 공급자를 만드는것이다. 여기서 생성한 공급자는 github actions내에서 실행된다
IAM OIDC ID 공급자를 만든 후에는 하나 이상의 IAM 역할을 만들어야한다
- AWS 콘솔 → IAM → ID 제공업체
- 공급자 추가
- OpenID Connect 선택 → 공급자 URL에 https://token.actions.githubusercontent.com 입력 → 대상에 sts.amazonaws.com 입력
- https://token.actions.githubusercontent.com 로 토큰의 발행자를 지정하는 것
OIDC와 연결된 IAM 역할 추가 [링크]
github actions는 ID 제공업체에서 발급받은 JWT 토큰을 클라우드 서비스에 전달한다 (여기선 AWS)
AWS는 토큰의 서명, 발행자, 만료시간 등 여러 조건을 검증하고, 인증에 성공하면 AWS는 인증된 요청에 대해 필요한 권한을 부여한다. 그 부여할 권한을 설정해주는 단계이다.
여기서 조건을 github 조직내의 특정 레파지토리 또는 브랜치 집합으로 제한하는것이 좋다
1. AWS 콘솔 → IAM → 역할 → 역할 생성
2. 웹 자격 증명 선택 → ID 제공업체 token.actions.githubusercontent.com 선택 → Audlence sts.amazonaws.com 선택 → github 조직명 작성 → 특정 레포만 하고 싶다면 github 레파지토리 지정
3. AmazonSSMManagedInstanceCore 권한 추가
4. 신뢰할 수 있는 엔터티에 아래와 같이 작성되었는지 확인
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::012345678910:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:GitHubOrg/GitHubRepo:*"
}
}
}
]
}
- repo:GitHubOrg/GitHubRepo:* 는 해당 조직의 레포의 모든 브랜치를 허용
- 01234567891 이 부분은 모두 다르게 표시될 것임
5. 역할 생성 완료
S3 버킷 만들기
왜 S3를 이용했는가?
- 우리가 배포 자동화를 진행하려는 대상 서버의 OS가 window인데 해당 서버로 github actions가 실행되는 가상머신에서 바로 파일을 복붙하는게 기본적으로 불가능하다. 그 이유는 아래와 같다
- github actions와 AWS EC2는 같은 네트워크를 쓰지 않는다. github actions는 보안상의 이유로 SSH 또는 RDP와 같은 프로토콜을 통해 직접 파일을 전송하지 않는다
- AWS SSM은 파일 전송 기능을 기본적으로 제공하지 않는다
- windows EC2 인스턴스는 기본적으로 ssh를 통한 파일 복사를 지원하지 않으며 별도의 추가 설정을 해줘야한다
- 애초에 github actions에서 build 폴더를 생성하지 않고 원격 서버에서 작업을 수행하면 될 것 같은데 이는 이미 우리의 제약 조건이랑 안맞음
- 여러가지 해결 방법들이 있지만 우린 AWS를 쓰고 있고 이미 만들어둔 S3가 있기에 이걸 활용하기로 함
SSM 및 S3 버킷에 액세스 하기위한 추가 설정
1. 생성한 역할 클릭
2. 권한 정책 → 권한추가 → 인라인 정책 생성
3. JSON 선택
4. S3 버킷 권한 설정
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::버킷_이름",
"arn:aws:s3:::버킷_이름/*"
]
}
]
}
- S3 버킷에서 파일을 읽기, 조회, 업로드 및 다운로드할 수 있는 권한을 부여
5. S3 권한 생성 완료
6. SSM 명령 실행 권한 설정
- SSM이란? → AWS에서 제공하는 관리 도구로, 클라우드 및 온프레미스 환경에서 리소스를 통합적으로 관리, 자동화, 모니터링 할 수 있도록 지원한다.
- SSM의 주요 기능으로 터미널 명령 실행, 자동화 관련 작업, ssh key없이 서버 접속 등등이 있다
- 우리가 SSM을 써야했던 이유는 대표적으로 windows 인스턴스에 기본적으로 SSM Agent가 설치되어 있어서 SSM의 주요 기능을 쓸 수 있기 때문이다. 우리는 특히 터미널 명령어를 직접 실행해서 IIS를 통해 특정 도메인을 재실행 해야했기 때문
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:SendCommand",
"ssm:GetCommandInvocation"
],
"Resource": [
"arn:aws:ec2:region:account-id:instance/instance-id"
]
}
]
}
- SSM은 EC2 인스턴스에 명령을 실행하기 위한 권한
- ssm:SendCommand → systems manager 명령 실행 작업을 허용
- ssm:GetCommandInvocation → 명령 실행의 결과와 상태를 조회하는 작업을 허용 (로그용)
🔥 여기서 문제 발생!
arn:aws:ec2:region:account-id:instance/instance-id 이와 같이 특정 인스턴스만 허용하도록 설정했더니 계속해서 ssm:SendCommand 권한이 없다고 뜨는것이다… 아무리 설정해도 계속 왜 안되는 것인지 모르겠다
결국 Resource를 “*” 로 줌으로서 모든 EC2 인스턴스와 SSM Document 모든 권한을 허용시켜버렸다
이건 딱히 좋지 못한 보안인데 걱정이 많다
해당 레파지토리 환경변수 설정
참고로 환경변수 설정하려면 권한이 있어야한다
민감 정보는 모두 위에서 설정한다
github actions 워크플로우 파일 작성
- 해당 프로젝트 root에 .github/workflows/(원하는 이름).yml 파일 생성
- 코드 작성
name: develop branch auto ci process script
on:
push:
branches: [ develop ] # 추적할 브랜치명 작성. 여기서는 develop 브랜치
permissions:
id-token: write # OIDC 토큰을 가져오기 위한 권한 설정
contents: read
jobs:
deploy:
name: develop deploy
runs-on: ubuntu-latest # 실행될 가상 머신 설정
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup Node.js # 가상 머신에 node 설치
uses: actions/setup-node@v3
with:
node-version: '23.4.0'
- name: Install Yarn # 우리는 yarn을 통해 패키지를 관리하니 yarn 설치
run: npm install -g yarn
- name: Install Dependencies
run: yarn install
- name: Create .env.development file # 미리 레포 환경변수에 설정한 내용을 가상머신 내부로 복사
run: |
echo "${{ secrets.ENV_DEVELOPMENT }}" > .env.development
- name: Build CRA
run: CI=false yarn build:dev # CI=false를 주지 않으면 type 워딩으로 빌드 실패
- name: Compress Build Folder
run: zip -r build.zip ./build # 빌드된 파일을 압축
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2 # configure-aws-credentials를 사용하여 OIDC 토큰을 기반으로 AWS에 인증
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Upload Build ZIP to S3
run: |
aws s3 cp build.zip s3://${{ secrets.S3_BUCKET_URL }}/build.zip --region ${{ secrets.AWS_REGION }}
- name: Excuting Command on EC2
run: |
aws ssm send-command \
--document-name "AWS-RunPowerShellScript" \
--targets "Key=instanceids,Values=${{ secrets.AWS_INSTANCE_ID }}" \
--parameters '{"commands":["$domainPath = \"원하는 경로"","$backupFolderBase = \"$domainPath\\build\"","$timestamp = (Get-Date -Format \"yyyyMMdd_HHmmss\")","$backupFolder = \"$backupFolderBase_$timestamp\"","if (Test-Path $backupFolderBase) { Rename-Item -Path \"$backupFolderBase\" -NewName \"$backupFolder\" -ErrorAction SilentlyContinue }","$s3Bucket = \"${{ secrets.S3_BUCKET_URL }}\"","$sourceFile = \"$domainPath\\build.zip\"","aws s3 cp \"s3://$s3Bucket/build.zip\" \"$domainPath\\\" --region ${{ secrets.AWS_REGION }}","Expand-Archive -Path \"$domainPath\\build.zip\" -DestinationPath \"$domainPath\\\" -Force","Remove-Item -Path \"$domainPath\\build.zip\" -Force","Copy-Item -Path \"$domainPath\\web.config\" -Destination \"$domainPath\\build\" -Force","Import-Module WebAdministration; Restart-WebAppPool -Name \"도메인\"","Import-Module WebAdministration; Restart-WebItem -PSPath \"IIS:\\Sites\\도메인\""]}' \
--region ${{ secrets.AWS_REGION }}
3. Excuting Command on EC2 구간에서 —parameters 부분은 내가 수행할 터미널 명령어를 입력해주면 된다
# build 폴더가 존재해야하는 경로
$domainPath = \"경로\"
# 백업 대상이 될 파일
$backupFolderBase = \"$domainPath\\build\"
# 백업 파일의 이름 변수 (현재 날짜와 시간)
$timestamp = (Get-Date -Format \"yyyyMMdd_HHmmss\")
$backupFolder = \"$backupFolderBase_$timestamp\"
# build 폴더가 있으면? 이름 변경
if (Test-Path $backupFolderBase) { Rename-Item -Path \"$backupFolderBase\" -NewName \"$backupFolder\" -ErrorAction SilentlyContinue }
# s3 버킷 이름과 리소스 위치
$s3Bucket = \"${{ secrets.S3_BUCKET_URL }}\"
$sourceFile = \"$domainPath\\build.zip\"
# 해당 S3 파일을 원격서버의 지정 경로로 다운로드 및 압축 해제
aws s3 cp \"s3://$s3Bucket/build.zip\" \"$domainPath\\\" --region ${{ secrets.AWS_REGION }}
Expand-Archive -Path \"$domainPath\\build.zip\" -DestinationPath \"$domainPath\\\" -Force
# 압축 파일 삭제
Remove-Item -Path \"$domainPath\\build.zip\" -Force
# 압축 해제한 폴더내에 web.config 파일을 복붙
Copy-Item -Path \"$domainPath\\web.config\" -Destination \"$domainPath\\build\" -Force
# IIS에서 해당 도메인 재시작
Import-Module WebAdministration; Restart-WebAppPool -Name \"도메인\"
Import-Module WebAdministration; Restart-WebItem -PSPath \"IIS:\\Sites\\도메인\"
(추가사항) AWS Systems Manager 설정
SSM을 사용하려면 AWS 인스턴스가 Systems Manager에 등록? 관리? 상태가 되어야한다 우리 인스턴스 같은 경우 설정을 안해놨기에 설정을 해줬다
아마 AWS Systems Manager 설정에 들어가서 활성화? 버튼을 눌러주면 알아서 뭔가 챠챠ㅑㄱ… 될 것이다
그 이후론 별도의 설정을 안해준걸로 기억한다
해당 리전에서 Systems Manager에 등록된 관리 인스턴스가 있는지 확인하려면 원격 서버에서 아래와 같이 터미널 명령어를 쳐본다
aws ssm describe-instance-information --region <region>
만약 관리되는게 없다면 아래처럼 빈 배열로 뜰 것이다
{
"InstanceInformationList": []
}
(추가사항) EC2 인스턴스와 IAM 역할 연결
문서들을 보면 별도로 인스턴스와 IAM을 직접 연결해줘야 한다는 문서를 본 것 같진 않은데.. 내 여러 시도 결과… 연결을 해줘야했었다 (아닐수도 있음 너무 많은 에러를 접하다가 여러가지 시도해보니 이 방법도 진행했었음)
해당 window 인스턴스를 보면 IAM 역할에 내가 생성했던 IAM과 연결을 해야하는데 애초에 선택 목록에 뜨질 않았던 이슈가 있었다
그래서 결국 직접 해당 인스턴스 서버에 들어가서 강제로 연결시켰다
1. 해당 원격 서버에 AWS CLI 설치
- AWS 계정과의 인증에 필요한 자격 증명 등록
- 이 과정에서 IAM 사용자를 생성해야할 수 있음
- 우리는 쓰고있는 사용자가 없어서 새로 생성
- EC2 인스턴스와 IAM 역할을 연결하려면 권한이 필요 AmazonEC2FullAccess 정책 추가, 아래 내용 인라인 정책 JSON으로 추가
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:CreateInstanceProfile",
"iam:AddRoleToInstanceProfile",
"iam:PassRole",
"iam:GetInstanceProfile",
"iam:RemoveRoleFromInstanceProfile",
"iam:DeleteInstanceProfile",
"ec2:AssociateIamInstanceProfile",
"ec2:DescribeIamInstanceProfileAssociations",
"ssm:DescribeInstanceInformation",
],
"Resource": "*"
}
]
}
2. 원격서버에 인스턴스 프로파일 생성
aws iam create-instance-profile --instance-profile-name MySSMInstanceProfile
3. IAM 역할과 인스턴스 프로파일 연결
aws iam add-role-to-instance-profile `
--instance-profile-name MySSMInstanceProfile `
--role-name MySSMRole
4. EC2 인스턴스에 인스턴스 프로파일 연결
aws ec2 associate-iam-instance-profile `
--instance-id i-123456789 `
--iam-instance-profile Name=MySSMInstanceProfile
🤪 여러가지 문제 상황들
추가적으로 윈도우 환경에서는 여러가지 예상치 못한 변수가 많았다 특히 aws 또는 ssm 터미널 명령어가 인식되지 않는 상황들을 자주 마주했는데 그럴때 재설치 하는 과정도 거쳤었다
[링크]
최종 결과
아무튼 IP 문제를 해결하고 터미널이 까다로운 window 환경에서 무사히 배포 자동화를 수행했다(?)
과정에서도 드러나듯 나는 AWS, 보안에 대해 무지한편이다 그래서 1개의 방법이 안되면 모든 권한을 줘보면서 테스트할 수 밖에 없었고... 뭔가를 이해하고 설정한 것도 아니다... 그래서 이 부분은 차츰차츰 1개씩 권한을 빼가면서 최소한의 권한만 유지하려고 시도해보고 있다.
그치만 잘 안된다^^!!ㅋㅋㅋㅋ... 대체 왜 안되냐고... 계속 권한이 없대...
그리고 이 과정에서 한가지 실패한게 또 있는데 S3에서 최신 파일을 액세스 하는게 문제다. 내가 원했던 이상향은 S3에 동적으로 이름이 할당된 build 압축본을 업로드하고 원격 서버에서는 가장 최신으로 업로드된 압축본을 내려받길 원했는데 계속 이게 안된다ㅠㅠㅠㅠ 웃긴건 에러가 1도 안뜨고 github actions가 success 되길래 서버 들어가서 확인해보면 정작 빈깡통 build 폴더만 남아있는 것이다...
그래서 어쩔수 없이 S3에는 build.zip라는 이름으로 계속 파일을 덮어씌우는 형태로 가져가고, 서버에서 무한히 백업 폴더를 만드는 형태로 진행하게 되었다. 난 백업본 개념을 S3로 가져가고 싶었는데... 이건 아쉽다. 방법을 찾기전까지는 당분간 수동으로 오래된 build폴더를 삭제해줘야 할 것 같다. 그리고 더불어 rollback도 어떤식으로 처리할지 고민해보는 것도 좋을 것 같다
OpenID Connect 개념을 이번에 처음 알게됐는데 이런건 네트워크 개념이 기본 베이스라 내가 깊이있게 이해하기란 어려웠다. 물론 알려주니까 아~ 그런거구나~는 하는데... 앞서 말했다시피 1개 터지면 다른 방법을 모른다는것...ㅎㅋ... 그리고 생각보다 많이 대중적이지 않아서 다른 사람들의 도입기, 실패경험 등의 자료들을 구하기 어려웠다. 챗gpt 선생님 덕분에 그나마 마무리지을 수 있었다..습습후후...
'Frontend' 카테고리의 다른 글
[React] 우린 왜 state, function, effect 순으로 작성했는가 (2) | 2025.01.16 |
---|---|
Jenkins와 Docker를 이용한 Next.js 프로젝트 배포 (3) | 2024.10.09 |
앵귤러 angular 핫 리로드, 빌드 시간 개선 (HMR) (1) | 2024.09.20 |
useCallback의 오남용 (2) | 2024.09.07 |
이벤트 리스너(event listener)를 제거 해야하는 이유 (1) | 2024.08.30 |
댓글