개발이군고구마

[멱살잡고 DevOps] 6. GitHub Actions CI/CD 구축 본문

Cloud Deploy

[멱살잡고 DevOps] 6. GitHub Actions CI/CD 구축

김구황 2025. 7. 23. 22:15
728x90
[Backend Dockerfile] 
1. backend Dockerfile 작성 🎆 
[AWS Front 설정] 
2. S3 버킷 & CloudFront 정적 웹 호스팅 활성화 
[AWS EC2 설정]
3. EC2 인스턴스 프로비저닝
4. EC2 환경 구성 🎆
[CI/CD] <- 현 단계
5. GitHub Actions CI/CD 구축 🎇
[테스트와 확장]
6.DNS 설정 및 최초 배포 테스트
7. 배포환경 분리 (dev/prod)

 

코드를 main 브랜치에 push 하기만 하면, 자동으로 테스트, 빌드, 배포가 완료

 

🤔 왜 CI/CD가 필요한가? (CI/CD의 철학)

 

CI (Continuous Integration, 지속적 통합): 여러 개발자가 작성한 코드를 주기적으로 통합하고, 자동으로 테스트하여 코드의 충돌이나 버그를 조기에 발견하는 개발 프랙티스

코드를 push할 때마다 자동으로 테스트와 린트(코드 스타일 검사)를 실행

 

CD (Continuous Deployment, 지속적 배포): CI 단계를 통과한 코드를 자동으로 실제 운영 환경까지 배포하는 것을 의미

이를 통해 개발자는 코드 작성에만 집중할 수 있고, 배포 과정의 반복적인 수작업과 인적 실수를 완전히 제거

 


1. GitHub Secrets 설정: 민감 정보 안전하게 보관하기

1) 백엔드 / 프론트 IAM 분리 

ㄴ 현관문 열쇠(프론트엔드 권한)와 자동차 키(백엔드 권한)는 따로 만드는 원리

 

  • 프론트엔드 파이프라인의 역할: 코드를 빌드해서 S3에 올리고(s3:Sync), CloudFront 캐시를 무효화(cloudfront:CreateInvalidation)하는 것
  • 백엔드 파이프라인의 역할: EC2 인스턴스에 접속해서 배포 스크립트를 실행(ssm:SendCommand)하는 것

 

✅ 역할 + 정책 과 사용자 + 정책의 차이 원리
  • 사람은 사용자(User)
    • 사람이 직접 로그인해서 작업
  • 서비스나 인프라는 역할(Role) 을 통해 리소스에 접근하도록 분리해서 관리
    • 사람은 아니지만 역할(Role)을 통해 권한을 임시로 획득

 

루트

- EC2에 역할 부여 -> 정책 연결 

SSM 접속과 cloudwatch log 확인

 

 

프론트 IAM 사용자 

- 정책 

 

 

백엔드 IAM 사용자 

- 정책

 

 

 

 

2) Secrets 등록

 

 

 

2. 백엔드 CI/CD 워크플로우 작성

 

1) .dockerignore로 최종 배포 패키지 최적화

💢  Docker 에 올릴 이미지, 소스코드 전체를 Docker 데몬으로 보내게 됨 (!=빌드와 별개)
  • __test__ 폴더, node_modules, .git 폴더 등 실제 서비스 실행에 필요 없는 파일까지 전부 포함됨
  • 이미지 용량 증가, 빌드 속도 저하, 보안 위험 등의 문제가 발생함 
  • 빌드(주방) 는 __test__ 폴더 포함하며, docker hub 에 올릴 이미지(포장) 는 dockerignore를 제외 

 

2) GitHub Actions 조건부 실행

✅ 1단계 PR 라벨 _ PR단계에서 코드의 품질을 자동으로 검증해줌 (마치, STG에 MR를 작성할 때 빌드 확인해주는 원리) 

 

🔮 라벨 방식

  • 아직 머지되지 않은 Pull Request에 특정 행동을 촉발시킬 때 매우 유용
  • 코드 스타일: 정해진 코딩 규칙(Lint)을 잘 지켰는가?
  • 기본 테스트: 기존의 기능들이 고장 나지 않았는가? (Unit/Integration Test)
  • 빌드 가능 여부: 이 코드가 실제로 실행 가능한 상태인가? (Build Check)

기본적으로 unit + label이 있다면 integration 

  • 선택적 테스트 실행: PR이 "integration-tests" 라는 라벨을 붙이면 run-db-integration-tests 라벨을 붙여 무거운 통합 테스트를 수동으로 실행
  • 자동화된 분류: PR의 내용에 따라 봇이 자동으로 bug나 enhancement 같은 라벨을 붙여주는 등의 작업을 할 때 사용

pull-request-check.yml

 

 

✅ 2단계 main push 태그 _ 두 개의 잡(Job)으로 분리하고, 조건부 실행 (if) 과 잡 의존성 (needs) 을 활용 (PRD_20250725)

 

🎴 태그 방식

  • 특정 코드 버전(커밋)을 공식적으로 지정하고 중요한 작업을 할 때 
  • 예) 운영/스테이징 서버 배포 - v1.0.0, v1.1-stg 명확한 버전을 지정하여 실제 서버에 배포할 때 사용 
  • "최종 배포" 에는 태그 방식이 압도적 
  • GitHub 웹사이트에서 직접 태그 생성 또는 개발자가 push 할 때 tag 생성 후 push tag
# git tag -a [태그이름] -m "[태그에 대한 설명]"
git tag -a test-v1.0 -m "회원가입 기능 테스트 및 배포"

# git push [원격저장소이름] [태그이름]
git push origin test-v1.0

 

backend-ci-cd.yml

frontend-ci-cd.yml

 

2단계 main push -> github action ci/cd EC2 배포 완료 / frontend main push S3 배포 완료

 

 

 

현재까지 진행상황

 


3. 발생 오류 

1) 백엔드

Error: Unable to resolve action aws-actions/aws-ssm-send-command@v1, action not found
 
✅ GitHub Actions 동작 원리

 

 

🌐 cicd 스크립트 github action 메뉴얼로 수정 
- name: AWS SSM Command
uses: Castlenine/aws-ssm-command@v1
id: ssm
env:
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # You can inject any environment variable in the command execution. Don't use "-" for the environment name. Use "_" instead. Make sure that your environment variables are not creating a conflict. Any environment variable with "SSM_IGNORE" in the name will not be exported in the command execution
with:
  aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
  aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  aws-region: ${{ secrets.AWS_REGION }}
  instance-ids: ${{ secrets.INSTANCE_ID }}
  working-directory: '/home/ubuntu'
  script-parent-folder-path: '.github' # Can be the main parent folder or with subfolders
  command: '.github/scripts/example.sh' # Must be in the parent folder path
  comment: 'Bash script executed by Github Actions'

 

 

 

Error: Failed to send or execute SSM command: User: arn:aws:iam::***:user/backend-deploy-user is not authorized to perform: ssm:SendCommand on resource: arn:aws:ssm:***::document/AWS-RunShellScript because no identity-based policy allows the ssm:SendCommand action

ㄴ AWS-RunShellScript는 “AWS 퍼블릭(관리) 문서”

✅ GitHub Actions ↔ AWS 호출 원리
  • configure-aws-credentials@v2
    • IAM 키를 받아 워크플로우 러너 환경 변수로 설정
    • 이후 모든 AWS API 호출(aws cli 또는 SSM 액션)이 이 크레덴셜을 사용해 요청을 서명(Signature V4)
  • Identity-based Policy
    • “내 액세스 키(Principal)” → “ssm:SendCommand(Action)” → “문서·인스턴스(Resource)”
    • 명시적 Allow 없으면 디폴트 거부(Deny)
  • 리소스 ARN 매칭
    • 정책의 ARN과 실제 호출 대상 ARN이 정확히 일치해야 매칭
    • 퍼블릭(관리) 리소스는 계정 ID 와일드카드(*)를 활용

 

Error: Failed to send or execute SSM command: User: arn:aws:iam::***:user/backend-deploy-user is not authorized to perform: ssm:GetCommandInvocation on resource: arn:aws:ssm:***:***:* because no identity-based policy allows the ssm:GetCommandInvocation action

ㄴ IAM 정책의 Resource 범위를 잘못 지정

✅  IAM 정책의 'Resource' 범위
  • SSM SendCommand는 '비동기(Asynchronous)'로 동작
  • AWS IAM은 “API 호출(Action)” 단위로 권한을 체크
  • 명령 전송 (ssm:SendCommand): GitHub Actions가 EC2에 "이 명령어들을 실행해줘!"라고 요청. 이것은 '택배를 발송하는 행위'
  • 결과 확인 (ssm:GetCommandInvocation): 요청을 보낸 후, GitHub Actions는 EC2에서 그 명령이 잘 실행되었는지, 성공했는지, 실패했는지 결과를 계속 확인(Polling). 이것은 '내 택배가 잘 도착했는지 배송 조회를 하는 행위'

 

🌐 IAM 정책 수정

 

기존

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": "ssm:SendCommand",
			"Resource": [
				"arn:aws:ssm:ap-northeast-2:{내계정}:document/AWS-RunShellScript",
				"arn:aws:ec2:ap-northeast-2:{내계정}:instance/{인스턴스아이디}
			]
		}
	]
}
  • AWS-RunShellScript는 “AWS 퍼블릭(관리) 문서”  ARN과 매치되지 않아 Policy에서 거부
{
    "Effect": "Allow",
    "Action": [
        "ssm:ListCommands",
        "ssm:ListCommandInvocations",
        "ssm:GetCommandInvocation"
    ],
    "Resource": [
        "arn:aws:ssm:ap-northeast-2:{내계정}:command/*/*" // ◀️ 이 부분이 문제
    ]
}
  • GetCommandInvocation 액션은 특정 command-id에 대해 실행되는데, command/*/* 라는 ARN 패턴은 유효하지 않아 매칭되지 않음
  • 액션의 실행 결과를 확인하는 권한은, 그 결과물(자원)이 생성되기 전이라 정확한 ARN을 예측하기 어려워 범위를 좁게 설정하기가 까다로움

 

수정후

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowSendCommandToSpecificInstance",
            "Effect": "Allow",
            "Action": "ssm:SendCommand",
            "Resource": [
                "arn:aws:ssm:ap-northeast-2:*:document/AWS-RunShellScript",
                "arn:aws:ec2:ap-northeast-2:{내계정}:instance/{인스턴스ID}"
            ]
        },
        {
            "Sid": "AllowGetCommandInvocation",
            "Effect": "Allow",
            "Action": "ssm:GetCommandInvocation",
            "Resource": "*"
        }
    ]
}
  • 기존의 **명령을 보내는 권한(SendCommand)**은 보안을 위해 특정 인스턴스로 범위를 좁혀두고, **결과를 확인하는 권한(GetCommandInvocation)**은 모든 리소스("*")에 대해 허용

 


2) 프론트엔드

Error: fatal error: An error occurred (AccessDenied) when calling the ListObjectsV2 operation: User: arn:aws:iam::***:user/frontend-deploy-user is not authorized to perform: s3:ListBucket on resource: "arn:aws:s3:::***" because no identity-based policy allows the s3:ListBucket action Process completed with exit code 1.

ㄴ frontend-deploy-user의 IAM 정책에는 파일을 올리고 지우는 s3:PutObject, s3:DeleteObject 권한은 있지만,

S3 버킷의 파일 목록을 조회하는 s3:ListBucket 권한이 없어서 1번 단계에서 실패

ㄴ ! 버킷 이름 오타 

 

✅ aws s3 sync의 동작 원리
5. 빌드 결과물을 S3에 업로드 (sync)
- name: Deploy to S3
run: |
  aws s3 sync ./build s3://${{ secrets.AWS_S3_BUCKET_NAME }} --delete

 

aws s3 sync 명령어는 단순히 파일을 올리기만 하는 cp (copy) 명령어와 다르게 동작 

  • 대상 확인: 먼저 S3 버킷에 어떤 파일들이 있는지 목록을 확인. (이 단계에서 s3:ListBucket 권한이 필요)
  • 비교: 로컬의 build 폴더 내용과 S3 버킷의 파일 목록을 비교.
  • 작업 수행:
    • S3에 없는 새 파일은 업로드 (s3:PutObject)
    • S3에 있지만 로컬에 없는 파일은 삭제 (s3:DeleteObject, --delete 옵션 때문에)
    • 내용이 변경된 파일은 덮어쓰기 (s3:PutObject)

 

🌐 IAM 정책 수정

 

기존

{
    "Sid": "AllowS3ManageStaticSite",
    "Effect": "Allow",
    "Action": [
        "s3:ListBucket",
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
    ],
    "Resource": [
        "arn:aws:s3:::{버킷이름}",
        "arn:aws:s3:::{버킷이름}/*"
    ]
},

 

IAM 정책에서 버킷 자체에 대한 권한 버킷 안의 객체(파일)들에 대한 권한은 별개로 취급.

  • arn:aws:s3:::-bucket : 버킷 자체 (ListBucket 권한 필요)
  • arn:aws:s3:::-bucket/* : 버킷 안의 모든 객체 (PutObject, DeleteObject 등 권한 필요)
  • “버킷”과 “객체” 두 리소스에 모두 ListBucket을 적용해 버렸기 때문에 AWS 내부 검증에서 거부될 수 있음

 

수정 후 

{
    "Sid": "ListBucketForSync",
    "Effect": "Allow",
    "Action": "s3:ListBucket",
    "Resource": "arn:aws:s3::: {버킷이름}"
},
{
    "Sid": "ManageBucketObjects",
    "Effect": "Allow",
    "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
    ],
    "Resource": [
        "arn:aws:s3:::{버킷이름}/*"
    ]
},