# Git Actions์™€ Docker๋ฅผ ์ด์šฉํ•œ CI/CD pipeline ๊ตฌ์ถ•
Study Repository

Git Actions์™€ Docker๋ฅผ ์ด์šฉํ•œ CI/CD pipeline ๊ตฌ์ถ•

by rlaehddnd0422

์ด ํฌ์ŠคํŒ…์—์„œ๋Š” Docker์™€ Git Action์„ ์‚ฌ์šฉํ•˜์—ฌ CI/CD๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค.

 

CI/CD๋Š” ์ง€์†์  ํ†ตํ•ฉ(Continuous Integration) ๋ฐ ์ง€์†์  ์ œ๊ณต/๋ฐฐํฌ(Continuous Delivery/Deployment)๋ฅผ ์˜๋ฏธํ•˜๋ฉฐ, ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ๋ผ์ดํ”„์‚ฌ์ดํด์„ ๊ฐ„์†Œํ™”ํ•˜๊ณ  ๊ฐ€์†ํ™”ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

 

  • ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋ฅผ pushํ•˜๊ณ , ๋ฆฌ๋ชจํŠธ ์„œ๋ฒ„์—์„œ pullํ•˜๊ณ  buildํ•˜๊ณ  runํ•˜๋Š” ์ผ๋ จ์˜ ๊ณผ์ •๋“ค์„ ์ž๋™ํ™”ํ•œ๋‹ค๋ฉด, ๊ฐœ๋ฐœ์ž๋Š” ๊ฐœ๋ฐœ์—๋งŒ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๊ฒ ์ฃ ??
  • ์ด๋ ‡๊ฒŒ push -> pull -> build -> run ํ•˜๋Š” ๊ณผ์ •๋“ค์„ ํ•˜๋‚˜์˜ ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ๊ตฌ์ถ•ํ•˜์—ฌ ์ž๋™ํ™”ํ•˜๋Š” ๊ฒƒ์„ CI/CD๋ผ๊ณ  ์ƒ๊ฐํ•˜์‹œ๋ฉด ๋˜๊ฒ ์Šต๋‹ˆ๋‹ค!

CI/CD ์ค‘ ์ง€์†์  ํ†ตํ•ฉ(CI)์€ ์ฝ”๋“œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๊ณต์œ  ์†Œ์Šค ์ฝ”๋“œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์— ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์„, ์ง€์†์  ์ œ๊ณต(CD)๋Š” ์ฝ”๋“œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์˜ ํ†ตํ•ฉ๋ฏผ ์ œ๊ณต, ๋ฐฐํฌ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ํ”„๋กœ์„ธ์Šค๋กœ CI์™€ CD ๋‘ ๊ฐ€์ง€ ๋ถ€๋ถ„์œผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

 

์กฐ๊ธˆ ๋” ๊ฐ„๋‹จํžˆ ๋งํ•˜์ž๋ฉด ์—…๋ฐ์ดํŠธ ๋œ ์ฝ”๋“œ๋ฅผ ์„œ๋ฒ„์— ํ†ตํ•ฉ ํ•˜๋Š” ๊ณผ์ •์„ CI, ํ†ตํ•ฉ๋œ ์ฝ”๋“œ๋ฅผ ๋ฐฐํฌํ•˜๋Š” ๊ณผ์ •์„ CD๋ผ๊ณ  ๋ณด์‹œ๋ฉด ๋˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

CI/CD๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ Jenkins, Travis CI ๋“ฑ ๋งŽ์€ ๋„๊ตฌ๋“ค์ด ์žˆ์ง€๋งŒ, ์ด ํฌ์ŠคํŒ…์—์„œ๋Š” Git Action์„ ์‚ฌ์šฉํ•˜์—ฌ CI/CD๋ฅผ ๊ตฌ์ถ•ํ•ด๋ณด๋ คํ•ฉ๋‹ˆ๋‹ค.

 

+ ์ถ”๊ฐ€์ ์œผ๋กœ ํ•„์ž๋Š” Docker๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ์ ํŠธ์˜ ์ด๋ฏธ์ง€๋ฅผ ๋นŒ๋“œํ•˜๊ณ , ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋„์šฐ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ๋ฐฐํฌํ–ˆ์—ˆ๋Š”๋ฐ์š”. Docker๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ์ ํŠธ๋ฅผ ๋นŒ๋“œํ•˜๊ณ  ๋ฐฐํฌ๋Š” ์–ด๋–ป๊ฒŒ ํ–ˆ๋Š”์ง€๋Š” ์ถ”ํ›„์— ์ž‘์„ฑํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 


์•„ํ‚คํ…์ฒ˜ ๊ตฌ์กฐ

ํ”„๋กœ์ ํŠธ ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์กฐ

 

๋กœ์ปฌ ์ฝ”๋“œ ๋ณ€๊ฒฝ ๋ฐ Git Commit & Push

โ–ถ๏ธŽ Repository์—์„œ Push ๊ฐ์ง€

โ–ถ๏ธŽ Docker-Compose๋กœ ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ (CI) ๋ฐ Docker hub์— ๋นŒ๋“œํ•œ ์ด๋ฏธ์ง€ PUSH

โ–ถ๏ธŽ Git Action์—์„œ EC2 ์„œ๋ฒ„ ์ ‘๊ทผ

โ–ถ๏ธŽ Docker hub๋กœ๋ถ€ํ„ฐ ์ด๋ฏธ์ง€ PULL

โ–ถ๏ธŽ ๋นŒ๋“œํ•œ ์ด๋ฏธ์ง€๋กœ ์ปจํ…Œ์ด๋„ˆ ๊ตฌ์ถ• ๋ฐ ์ปจํ…Œ์ด๋„ˆ run (CD)

 

๊ทธ๋ ‡๊ฒŒ ๋ณต์žกํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด ๊ณผ์ •๋“ค์„ ์ง€๊ธˆ๋ถ€ํ„ฐ ๊ตฌ์ถ•ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


Workflow ๊ตฌ์„ฑ์š”์†Œ

 

ํ”„๋กœ์ ํŠธ์˜ .github/workflows ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ์ง์ ‘์ ์œผ๋กœ .yml ํŒŒ์ผ์„ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์ง€๋งŒ, ์•„๋ž˜์™€ ๊ฐ™์ด Github์—์„œ Actionsํƒญ์— ๋“ค์–ด๊ฐ€์„œ ์ž๋™์œผ๋กœ ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

Java with Gradle - Configure๋ฅผ ํด๋ฆญํ•˜์—ฌ ์ œ๊ณตํ•˜๋Š” ํ…œํ”Œ๋ฆฟ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด์„œ CI, CD๋ฅผ ๊ตฌ์ถ•ํ• ๊ฑด๋ฐ, Git Action์˜ ๊ตฌ์„ฑ์š”์†Œ๋“ค์„ ๋จผ์ € ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

Workflow(yml)

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle

name: Java CI with Gradle

on:
  push:
    branches: [ "develop" ]
  pull_request:
    branches: [ "develop" ]

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
    - uses: actions/checkout@v4
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'

    # Configure Gradle for optimal use in GiHub Actions, including caching of downloaded dependencies.
    # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

    - name: Build with Gradle Wrapper
      run: ./gradlew build

    # NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (https://docs.gradle.org/current/userguide/gradle_wrapper.html).
    # If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version.
    #
    # - name: Setup Gradle
    #   uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
    #   with:
    #     gradle-version: '8.5'
    #
    # - name: Build with Gradle 8.5
    #   run: gradle build

  dependency-submission:

    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
    - uses: actions/checkout@v4
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'

    # Generates and submits a dependency graph, enabling Dependabot Alerts for all project dependencies.
    # See: https://github.com/gradle/actions/blob/main/dependency-submission/README.md
    - name: Generate and submit dependency graph
      uses: gradle/actions/dependency-submission@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
  • ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ์ผ๋ จ์˜ ์ž๋™ํ™”๋œ ์ปค๋งจ๋“œ ์ง‘ํ•ฉ์œผ๋กœ ์œ„ yml ํŒŒ์ผ์„ ์ผ์ปฌ์–ด workflow๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๊ฒ ์Šต๋‹ˆ๋‹ค.

 

Event

on:
  push:
    branches: [ "develop" ]
  pull_request:
    branches: [ "develop" ]
  • Workflow๊ฐ€ ๊ตฌ๋™์‹œํ‚ค๊ธฐ ์œ„ํ•œ ํŠธ๋ฆฌ๊ฑฐ๋กœ, ์—ฌ๊ธฐ์— ์ž‘์„ฑ๋œ ๋™์ž‘๋“ค์ด ์ด๋ฃจ์–ด ์งˆ ๋•Œ Workflow๊ฐ€ ๊ตฌ๋™๋ฉ๋‹ˆ๋‹ค.
  • ์œ„์™€ ๊ฐ™์ด push, pull_request์™€ ๊ฐ™์ด ํŠน์ •ํ•œ ์ž‘์—…์ด ์ด๋ฃจ์–ด์งˆ ๋•Œ workflow๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์œ„ ์˜ˆ์‹œ์—์„œ๋Š” develop ๋ธŒ๋žœ์น˜์— pushํ•˜๊ฑฐ๋‚˜ develop ๋ธŒ๋žœ์น˜์— PR์„ ๋ณด๋‚ผ ๋•Œ ์ด workflow๊ฐ€ ๋™์ž‘ํ•œ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ๊ฒ ์ฃ .

 

Runner

  • Workflow์˜ ๋™์ž‘๋“ค์„ ๊ตฌ๋™ํ•˜๊ธฐ ์œ„ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ, Github์—์„œ ๊ฐ€์ƒ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•˜์—ฌ ์ด ๊ฐ€์ƒ ํ™˜๊ฒฝ ์œ„์—์„œ workflow๊ฐ€ ๊ตฌ๋™๋œ๋‹ค๊ณ  ๋ณด์‹œ๋ฉด ๋˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

Action

  • ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Workflow์˜ ๊ฐ€์žฅ ์ž‘์€ ๋‹จ์œ„๋ธ”๋Ÿญ์œผ๋กœ, ์ง์ ‘๋งŒ๋“  Action์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ Github ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์˜ํ•ด ์ƒ์„ฑ๋œ Action์„ ๋ถˆ๋Ÿฌ์™€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ•„์ž๊ฐ€ ์ž‘์„ฑํ•œ Workflow์—์„œ๋Š” ์‚ฌ์šฉ๋˜์ง€๋Š” ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฐ ๊ตฌ์„ฑ์š”์†Œ๋„ ์žˆ๋‹ค ์ •๋„๋กœ ์•Œ๊ณ ๋งŒ ๋„˜์–ด๊ฐ€์…”๋„ ์ข‹์Šต๋‹ˆ๋‹ค.

 

Step

  • workflow์— ์ž‘์„ฑ๋œ ์‹ค์ œ ๊ตฌ๋™๋˜๋Š” ์ปค๋งจ๋“œ๋“ค์„ ๋ฌถ์–ด Step์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์Šคํƒญ์œผ๋กœ ๋ฌถ์—ฌ์„œ ์ž‘์„ฑ๋œ ์ปค๋งจ๋“œ๋“ค์€ ์Šคํ… ๋ฐ”์ด ์Šคํ…์œผ๋กœ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— CI/CD์— ํ•„์š”ํ•œ ์ž‘์—…๋“ค์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ง„ํ–‰์‹œํ‚ฌ ์ˆ˜ ์žˆ๊ฒ ์Šต๋‹ˆ๋‹ค.

 

Job

  • ์—ฌ๋Ÿฌ Step๋“ค์˜ ์ง‘ํ•ฉ์„ ํ•˜๋‚˜์˜ Job์œผ๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ณดํ†ต ํ•˜๋‚˜์˜ Workflow์— ์—ฌ๋Ÿฌ Job์ด ์žˆ๋‹ค๋ฉด ๊ฐ๊ฐ์˜ Job๋“ค์€ ์ˆœ์„œ์— ์ƒ๊ด€์—†์ด ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋˜์ง€๋งŒ, ํ•„์š”์— ๋”ฐ๋ผ ์˜์กด๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•˜์—ฌ ์ˆœ์„œ๋ฅผ ์ง€์ •ํ•  ์ˆ˜๋„ ์žˆ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.
  • ์ฐธ๊ณ ๋กœ CI์™€ CD๋ฅผ ๋‚˜๋ˆ„์–ด ๊ฐ๊ฐ์˜ Job์œผ๋กœ ์„ค์ •ํ•˜๊ณ , CI๊ฐ€ ๋๋‚œ ํ›„ CD๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ์˜์กด๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•  ์ˆ˜๋„ ์žˆ๊ฒ ์ง€๋งŒ, ํ•„์ž๋Š” ๊ตณ์ด ๊ทธ๋Ÿด ํ•„์š”์„ฑ์ด ์—†๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ, CI/CD๊ณผ์ •์„ ํ•˜๋‚˜์˜ Job์œผ๋กœ ๊ด€๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

CI/CD๋ฅผ ์œ„ํ•œ workflow - 0. ๋นŒ๋“œ ํ™˜๊ฒฝ ๊ตฌ์ถ•

name: Java CI with Gradle

on:
  push:
    branches: [ "develop" ]

jobs:
  deploy:
    runs-on: ubuntu-latest

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

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'adopt'

      - name: Copy Secret
        env:
          OCCUPY_SECRET: ${{ secrets.OCCUPY_SECRET }}
          OCCUPY_SECRET_DIR: src/main/resources
          OCCUPY_SECRET_DIR_FILE_NAME: application-secret.yml
        run: echo $OCCUPY_SECRET | base64 --decode > $OCCUPY_SECRET_DIR/$OCCUPY_SECRET_DIR_FILE_NAME

      - name: gradlew mod modify
        run: chmod +x gradlew

      # gradle ์บ์‹ฑ (0)
      - name: Gradle Caching
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      # Spring Boot ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ Build (1)
      - name: Spring Boot Build
        run: ./gradlew clean build --exclude-task test

      # Docker ์ด๋ฏธ์ง€ Build (2)
      - name: docker image build
        run: docker build -t rlaehddnd0422/dnd .

      # DockerHub Login (3)
      - name: docker login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      # Docker Hub push (4)
      - name: docker Hub push
        run: docker push rlaehddnd0422/dnd
      
      # GET GitHub IP (5)
      - name: get GitHub IP
        id: ip
        uses: haythem/public-ip@v1.2

      # Configure AWS Credentials (6) - AWS ์ ‘๊ทผ ๊ถŒํ•œ ์ทจ๋“(IAM)
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2
      
      # Add github ip to AWS (7)
      - name: Add GitHub IP to AWS
        run: |
          aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
      
      # AWS EC2 Server Connect & Docker ๋ช…๋ น์–ด ์‹คํ–‰ (8)
      - name: AWS EC2 Connection
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          password: ${{ secrets.EC2_PASSWORD }}
          port: ${{ secrets.EC2_SSH_PORT }}
          timeout: 60s
          script: |
            sudo docker stop dnd2
            sudo docker rm dnd2
            sudo docker rmi rlaehddnd0422/dnd
            sudo docker pull rlaehddnd0422/dnd
            sudo docker run -it -d -p 8080:8080 --name dnd2 rlaehddnd0422/dnd
      
      # REMOVE GitHub IP FROM security group (9)
      - name: Remove IP FROM security group
        run: |
          aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
  • ํ”„๋กœ์ ํŠธ์— CI/CD๋ฅผ ๊ตฌ์ถ•ํ•œ ์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค.
  • Step ๋ณ„๋กœ ํ•˜๋‚˜์”ฉ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ์ฝ”๋“œ๋ฅผ ๋œฏ์–ด๋ด…์‹œ๋‹ค.

 

0.  Event ์ž‘์„ฑ

on:
  push:
    branches: [ "develop" ]
  • PR์„ ๊ฐ์ง€ํ•˜์—ฌ PR ์‹œ์—๋„ ์ˆ˜ํ–‰๋˜๋„๋ก ์ž‘์„ฑํ•  ์ˆ˜๋„ ์žˆ๊ฒ ์ง€๋งŒ, ํ•„์ž๋Š” ๋™์ผํ•œ ์ž‘์—…์— ๋Œ€ํ•ด CI CD๋ฅผ PR์—์„œ ํ•œ ๋ฒˆ, merge์—์„œ ํ•œ ๋ฒˆ ์ด๋ ‡๊ฒŒ ์ด ๋‘ ๋ฒˆ ๋™์ž‘ํ•  ์ด์œ ๊ฐ€ ์—†๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ์ €๋Š” develop ๋ธŒ๋žœ์น˜์— pushํ•˜๋Š” event ๋ฐœ์ƒ ์‹œ์—๋งŒ workflow๊ฐ€ ๋™์ž‘ํ•˜๋„๋ก ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

1. Runner ํ™˜๊ฒฝ์— JDK 17 ์„ค์น˜

jobs:
  deploy:
    runs-on: ubuntu-latest

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

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'adopt'
  • Runner ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์šฐ๋ถ„ํˆฌ์˜ ๊ฐ€์žฅ ์ตœ๊ทผ ๋ฒ„์ „ ์œ„์—์„œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋นŒ๋“œํ•˜๊ธฐ ์œ„ํ•ด JDK 17์„ ์„ค์น˜ํ•ด์ค๋‹ˆ๋‹ค.

 

2. ์„ค์ •์ •๋ณด secret ํŒŒ์ผ ์ฃผ์ž…

 

ํ•„์ž๋Š” ์ฐธ๊ณ ๋กœ application.yml์— ์ž‘์„ฑ๋œ JWT ์‹œํฌ๋ฆฟ ํ‚ค์™€ ๊ฐ™์ด ๋ณด์•ˆ์„ฑ์ด ๋†’์€ ์ฝ”๋“œ๋“ค์„ application-secret.yml๋กœ ๊ด€๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. 

์œ ์ถœ๋˜๋ฉด ์•ˆ๋˜๋Š” ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์—” ์ด ๊ณผ์ •์€ ํŒจ์Šคํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

application.yml

jwt:
  secret_key: ${jwt.secret.secret_key}

 

application-secret.yml

jwt:
  secret:
    secret_key: ~~

 

- name: Copy Secret
  env:
    OCCUPY_SECRET: ${{ secrets.OCCUPY_SECRET }}
    OCCUPY_SECRET_DIR: src/main/resources
    OCCUPY_SECRET_DIR_FILE_NAME: application-secret.yml
  run: echo $OCCUPY_SECRET | base64 --decode > $OCCUPY_SECRET_DIR/$OCCUPY_SECRET_DIR_FILE_NAME
  • ํ”„๋กœ์ ํŠธ๋ฅผ ๋นŒ๋“œํ•˜๊ธฐ์— ์•ž์„œ, Git์—์„œ ๊ด€๋ฆฌํ•˜์ง€ ์•Š๋Š” ๋ณด์•ˆ์„ฑ์ด ๋†’์€ ์ฝ”๋“œ๋“ค์€ secret์œผ๋กœ ์ฃผ์ž…์‹œ์ผœ ์ค์‹œ๋‹ค.
  • ๋จผ์ € ์ฃผ์ž…ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ๋นŒ๋“œ๊ฐ€ ์ œ๋Œ€๋กœ ์ด๋ฃจ์–ด์ง€์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ๋ฐ˜๋“œ์‹œ ์ฃผ์ž…์„ ๋จผ์ € ์ˆ˜๋™์œผ๋กœ ํ•ด์ค์‹œ๋‹ค.

run: echo $OCCUPY_SECRET | base64 --decode > $OCCUPY_SECRET_DIR/$OCCUPY_SECRET_DIR_FILE_NAME

 

โ–ถ๏ธŽ run ์ปค๋งจ๋“œ๋ฅผ ํ†ตํ•ด ์ธ์ฝ”๋”ฉ๋œ secret ํŒŒ์ผ์„ base64๋กœ ๋””์ฝ”๋”ฉํ•˜์—ฌ SECRET_DIR ์— ์ž‘์„ฑํ•œ ๋””๋ ‰ํ† ๋ฆฌ์— FILE_NAME์œผ๋กœ ์„ธํŒ…ํ•ฉ๋‹ˆ๋‹ค. 

 

3. secret ํŒŒ์ผ ํ™˜๊ฒฝ ๋ณ€์ˆ˜์— ์„ธํŒ…ํ•˜๊ธฐ

Github ํ”„๋กœ์ ํŠธ - Setting ํƒญ

Github์—์„œ CI/CD๋ฅผ ๊ตฌ์ถ•ํ•  ํ”„๋กœ์ ํŠธ์˜ Setting ํƒญ์—์„œ Secrets and variables์˜ Actions ํƒญ์—์„œ Git Action์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

  • New Repository secret์„ ๋ˆŒ๋Ÿฌ ์›ํ•˜๋Š” ์ด๋ฆ„๊ณผ ์‹œํฌ๋ฆฟ ๊ฐ’๋“ค์„ ์„ค์ •ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

ex) ์„ค์ •ํ•œ ์‹œํฌ๋ฆฟ ํŒŒ์ผ

  • ์ฐธ๊ณ ๋กœ secret ํŒŒ์ผ์€ base64๋กœ ์ธ์ฝ”๋”ฉํ•˜์—ฌ ์„ค์ •ํ•˜๋„๋ก ํ•ฉ์‹œ๋‹ค.
  • ๋งํฌ ์ฐธ๊ณ  :  https://www.base64decode.org/
 

Base64 Decode and Encode - Online

Decode from Base64 format or encode into it with various advanced options. Our site has an easy to use online tool to convert your data.

www.base64decode.org

 


CI/CD๋ฅผ ์œ„ํ•œ workflow - 1. CI ๊ตฌ์ถ•

- name: gradlew mod modify
  run: chmod +x gradlew

# gradle ์บ์‹ฑ (0)
- name: Gradle Caching
  uses: actions/cache@v3
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
      ${{ runner.os }}-gradle-
  • Gradlew mod modify: Gradle ๋ž˜ํผ ์Šคํฌ๋ฆฝํŠธ์˜ ๊ถŒํ•œ์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.
  • Gradle Caching: Gradle ์ข…์†์„ฑ์„ ์บ์‹ฑํ•˜์—ฌ ์›Œํฌํ”Œ๋กœ์šฐ ์‹คํ–‰ ์‹œ๊ฐ„์„ ๋‹จ์ถ•ํ•ฉ๋‹ˆ๋‹ค.
# Spring Boot ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ Build (1)
- name: Spring Boot Build
  run: ./gradlew clean build --exclude-task test

# Docker ์ด๋ฏธ์ง€ Build (2)
- name: docker image build
  run: docker build -t rlaehddnd0422/dnd .

# DockerHub Login (3)
- name: docker login
  uses: docker/login-action@v2
  with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_TOKEN }}

# Docker Hub push (4)
- name: docker Hub push
  run: docker push rlaehddnd0422/dnd
  • Spring Boot Build: Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋นŒ๋“œํ•˜๋ฉฐ ํ…Œ์ŠคํŠธ๋Š” ์ œ์™ธํ•ฉ๋‹ˆ๋‹ค.
  • Docker ์ด๋ฏธ์ง€ build: rlaehddnd0422/dnd ํƒœ๊ทธ๋กœ Docker ์ด๋ฏธ์ง€๋ฅผ ๋นŒ๋“œํ•ฉ๋‹ˆ๋‹ค.
  • Docker login: ์ œ๊ณต๋œ ์ž๊ฒฉ ์ฆ๋ช…์„ ์‚ฌ์šฉํ•˜์—ฌ Docker Hub์— ๋กœ๊ทธ์ธํ•ฉ๋‹ˆ๋‹ค.
    • ์—ฌ๊ธฐ์„œ ์ œ๊ณตํ•œ ์ž๊ฒฉ ์ฆ๋ช… ๊ฐ’ ๋˜ํ•œ secret ํŒŒ์ผ์ฒ˜๋Ÿผ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ฃผ์ž…ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ณผ์ •์€ ๋™์ผํ•˜๋ฏ€๋กœ ์„ค๋ช… ์ƒ๋žต
  • Docker Hub push: Docker ์ด๋ฏธ์ง€๋ฅผ Docker Hub์— ํ‘ธ์‹œํ•ฉ๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ develop ๋ธŒ๋žœ์น˜์— push๊ฐ€ ๊ฐ์ง€๋˜๋Š” ๊ฒฝ์šฐ ํ”„๋กœ์ ํŠธ๋ฅผ ๋นŒ๋“œํ•˜๊ณ , ๋„์ปค ์ด๋ฏธ์ง€๋ฅผ ๋นŒ๋“œํ•˜์—ฌ ๋„์ปค ํ—ˆ๋ธŒ์— ํ‘ธ์‰ฌํ•˜๋Š” ๊ณผ์ •์ธ CI๊ณผ์ •์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. 

 


CI/CD๋ฅผ ์œ„ํ•œ workflow - 2. CD ๊ตฌ์ถ•

์ด์ œ ๋ฆฌ๋ชจํŠธ ์„œ๋ฒ„์—์„œ ๋„์ปค ํ—ˆ๋ธŒ์— ์žˆ๋Š” ์ด๋ฏธ์ง€๋ฅผ pullํ•˜์—ฌ runํ•˜๋Š” CD๊ณผ์ •๊นŒ์ง€ ๊ตฌ์ถ•ํ•ด๋ณผ๊ฑด๋ฐ, ๊ณผ์ •์ด ๋น„๊ต์  ๋ณต์žกํ•˜์ง€๋งŒ ์ž˜ ๋”ฐ๋ผ์˜จ๋‹ค๋ฉด ๊ทธ๋ ‡๊ฒŒ ์–ด๋ ต์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

์‚ฌ์ „์ž‘์—…์œผ๋กœ ๋จผ์ € Runner์—์„œ ๋ฆฌ๋ชจํŠธ EC2 ์„œ๋ฒ„์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด ๋ณด์•ˆ๊ทธ๋ฃน์„ ์—ด์–ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

EC2์˜ IAM Role์„ ํ†ตํ•ด ์™ธ๋ถ€ ์ ‘๊ทผ ๊ถŒํ•œ์„ ์—ด์–ด์ฃผ๊ธฐ ์œ„ํ•ด IAM Role์„ ์ƒ์„ฑํ•˜๋„๋ก ํ•ฉ์‹œ๋‹ค.

 

EC2 -> IAM ์„œ๋น„์Šค ์ ‘์†

 

์‚ฌ์šฉ์ž ์ƒ์„ฑ ํด๋ฆญ

 

์•„๋ž˜์™€ ๊ฐ™์ด ์„ ํƒ ํ›„ ๋‹ค์Œ ํด๋ฆญ

 

๊ถŒํ•œ ์„ค์ • - ๊ทธ๋ฃน ์ƒ์„ฑ

 

AmazonEc2FullAccess ์„ ํƒ ๋ฐ ์‚ฌ์šฉ์ž ๊ทธ๋ฃน ์ƒ์„ฑ

์‚ฌ์šฉ์ž ๊ทธ๋ฃน์„ ์ƒ์„ฑํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์•”ํ˜ธ๊ฐ€ ์ œ๊ณต๋˜๋Š”๋ฐ, ์•”ํ˜ธ๋Š” ์ด ๋•Œ ํ•œ๋ฒˆ๋งŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— csvํŒŒ์ผ๋กœ ๋‹ค์šด๋ฐ›์•„ ๊ด€๋ฆฌํ•˜๊ฑฐ๋‚˜, ์€๋ฐ€ํ•œ ๊ณณ์— ์ €์žฅํ•ด๋‘์ž.

๋‹ค์‹œ ์‚ฌ์šฉ์ž ํƒญ์œผ๋กœ ๋“ค์–ด์™€ ์•ก์„ธ์Šค ํ‚ค ๋งŒ๋“ค๊ธฐ๋ฅผ ํด๋ฆญํ•˜์—ฌ ์•ก์„ธ์Šค ํ‚ค๋ฅผ ๋ฐœ๊ธ‰๋ฐ›๋„๋ก ํ•ฉ์‹œ๋‹ค.
์šฉ๋„์— ๋งž๊ฒŒ ๋ฐœ๊ธ‰๋ฐ›์œผ๋ฉด ๋˜๋Š”๋ฐ, ์•„๋ฌด๊ฑฐ๋‚˜ ํ•ด๋„ ์ƒ๊ด€์—†๋Š” ๋“ฏ ํ•ฉ๋‹ˆ๋‹ค. ํ•„์ž๋Š” ์ฒซ๋ฒˆ์งธ CLI๋กœ ๋ฐœ๊ธ‰๋ฐ›์•˜์Šต๋‹ˆ๋‹ค.

 

๋ฐœ๊ธ‰๋ฐ›์€ ACCESS_KEY์™€ SECRET์„ ์•„๊นŒ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํ”„๋กœ์ ํŠธ ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ์„ธํŒ…ํ•ด์ฃผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

# GET GitHub IP (5)
- name: get GitHub IP
  id: ip
  uses: haythem/public-ip@v1.2

# Configure AWS Credentials (6) - AWS ์ ‘๊ทผ ๊ถŒํ•œ ์ทจ๋“(IAM)
- name: Configure AWS Credentials
  uses: aws-actions/configure-aws-credentials@v1
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: ap-northeast-2

 

EC2์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•œ IP๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , ์œ„์—์„œ ๋“ฑ๋กํ•œ id์™€ ์‹œํฌ๋ฆฟ ๊ฐ’์„ ์ฃผ์ž…ํ•˜์—ฌ, AWS EC2์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•œ IAM ๊ถŒํ•œ์„ ์–ป์Šต๋‹ˆ๋‹ค.

# Add github ip to AWS (7)
- name: Add GitHub IP to AWS
  run: |
    aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32

 

์ด์ œ ๊ฐ€์ ธ์˜จ ์•„์ดํ”ผ๋ฅผ AWS์— ์ ‘๊ทผํ•˜์—ฌ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก AWS์— ํ•ด๋‹น ๊นƒํ—ˆ๋ธŒ ์•„์ดํ”ผ๋ฅผ ๋“ฑ๋กํ•ด์ฃผ๊ณ , EC2์— ์ ‘๊ทผํ•˜์—ฌ Docker ๋ช…๋ น์–ด ์‰˜์„ ์‹คํ–‰ํ•˜์—ฌ CD ๊ตฌ์ถ•ํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

# AWS EC2 Server Connect & Docker ๋ช…๋ น์–ด ์‹คํ–‰ (8)
- name: AWS EC2 Connection
  uses: appleboy/ssh-action@v0.1.6
  with:
    host: ${{ secrets.EC2_HOST }}
    username: ubuntu
    password: ${{ secrets.EC2_PASSWORD }}
    port: ${{ secrets.EC2_SSH_PORT }}
    timeout: 60s
    script: |
      sudo docker stop dnd2
      sudo docker rm dnd2
      sudo docker rmi rlaehddnd0422/dnd
      sudo docker pull rlaehddnd0422/dnd
      sudo docker run -it -d -p 8080:8080 --name dnd2 rlaehddnd0422/dnd

 

ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ๋“ฑ๋กํ•œ EC2 ์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ, ๊ทธ๋ฆฌ๊ณ  ํฌํŠธ๋ฒˆํ˜ธ๋ฅผ ํ†ตํ•ด EC2์— ์ ‘๊ทผํ•˜์—ฌ docker ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ๋ฆฌ๋ชจํŠธ ์„œ๋ฒ„์— ์กด์žฌํ•˜๋Š” ๊ธฐ์กด์˜ ์ด๋ฏธ์ง€๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , ๋„์ปค ํ—ˆ๋ธŒ์—์„œ ์ด๋ฏธ์ง€๋ฅผ pullํ•˜๊ณ  ์ƒˆ๋กญ๊ฒŒ run ํ•ฉ๋‹ˆ๋‹ค.

# REMOVE GitHub IP FROM security group (9)
- name: Remove IP FROM security group
  run: |
    aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
  • CD ๊ตฌ์ถ•์ด ๋๋‚ฌ์œผ๋‹ˆ Github ์•„์ดํ”ผ๋ฅผ ๋ณด์•ˆ ๊ทธ๋ฃน์—์„œ ์ œ๊ฑฐํ•ด์คŒ์œผ๋กœ์จ ์ถ”ํ›„ ์™ธ๋ถ€ ์ ‘์†์„ ๋ง‰์•„์ฃผ๋„๋ก ํ•ฉ์‹œ๋‹ค.

CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•์— ์‚ฌ์šฉ๋œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ •๋ฆฌ 

  • AWS_ACCESS_KEY_ID : IAM Role์—์„œ ์ƒ์„ฑํ•œ ์•ก์„ธ์Šค ํ‚ค์˜ ID
  • AWS_ACCESS_KEY_PASSWORD : IAM Role๋กœ ์ƒ์„ฑํ•œ ์•ก์„ธ์Šค ํ‚ค์˜ PASSWORD

AWS_SG_ID : EC2 ๋ณด์•ˆ๊ทธ๋ฃน ID

  • AWS_SG_ID : EC2 ๋ณด์•ˆ๊ทธ๋ฃน ID
  • DOCKERHUB_PW : ๋„์ปค ํ—ˆ๋ธŒ ๋น„๋ฐ€๋ฒˆํ˜ธ
  • DOCKERHUB_TOKEN : ๋„์ปค ํ—ˆ๋ธŒ์—์„œ ๋ฐœ๊ธ‰๋ฐ›์€ ์•ก์„ธ์Šค ํ† ํฐ
  • DOCKERHUB_USERNAME : ๋„์ปค ํ—ˆ๋ธŒ์˜ ์•„์ด๋””
  • EC2_HOST : AWS EC2 ์ธ์Šคํ„ด์Šค์˜ ํผ๋ธ”๋ฆญ IPv4 DNS
  • EC2_PASSWORD : EC2์— ์ ‘๊ทผ/์—ฐ๊ฒฐ ์„ค์ •ํ•ด๋‘์—ˆ๋˜ ํŒจ์Šค์›Œ๋“œ 
  • EC2_SSH_PORT : SSH๋กœ ์ ‘๊ทผํ•  port. (22)
  • EC2_USERNAME : AWS EC2 ์ธ์Šคํ„ด์Šค์˜ username
  • OCCUPY_SECRET : application-secret.yml (encoded with base 64)

 

์ด๋ ‡๊ฒŒ ์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ Git Action์„ ์‚ฌ์šฉํ•˜์—ฌ CI/CD ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌํ˜„ํ•˜์—ฌ ๋ฐฐํฌ๋ฅผ ์ž๋™ํ™”ํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ๋Š” ์ด์–ด Docker๋ฅผ ์‚ฌ์šฉํ•ด ์–ด๋–ป๊ฒŒ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ด€๋ฆฌ(๋นŒ๋“œ/๋ฐฐํฌ)ํ–ˆ๋Š”์ง€ ์•Œ์•„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 


<์ฐธ๊ณ ์ž๋ฃŒ>

 

GitHub Actions๋ฅผ ์ด์šฉํ•œ CI/CD ๊ตฌ์ถ•ํ•˜๊ธฐ

Github Actions๋ฅผ ํ†ตํ•ด ์–ด๋–ป๊ฒŒ React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ž๋™์œผ๋กœ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ์•„๋ด…์‹œ๋‹ค.

ji5485.github.io

 

[Github Action, AWS] Github Action ์‚ฌ์šฉ์„ ์œ„ํ•œ AWS ํ‚ค ๋ฐœ๊ธ‰ ๋ฐ›๊ธฐ

github action์„ ํ™œ์šฉํ•ด์„œ EC2์™€์˜ CI/CD ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•  ๋•Œ, workflow์˜ .yml ํŒŒ์ผ์— secrets.AWS_ACCESS_KEY_ID, secrets.AWS_SECRET_ACCESS_KEY๋ฅผ ์ž…๋ ฅํ•ด์•ผํ•œ๋‹ค.

velog.io

 

[CI/CD] GitHub Action AWS์— IAM Role๋กœ ์ ‘๊ทผํ•˜๊ธฐ

์•ˆ๋…•ํ•˜์„ธ์š”! zerone-code์ž…๋‹ˆ๋‹ค. ์˜ค๋Š˜์€ ์•„๋งˆ ๋Œ€๋ถ€๋ถ„์˜ ๊ฐœ๋ฐœ์ž๋“ค์ด Github์„ ์ด์šฉํ•œ๋‹ค๋ฉด ์‚ฌ์šฉํ•ด๋ดค์„ GitHub Action์— ๋Œ€ํ•ด ๋งํ•ด๋ณด๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค. GitHub Action์„ ์ด์šฉํ•ด์„œ CI/CD๋ฅผ ๋งŽ์ด ํ•˜๋Š”๋ฐ, ์ด ๊ณผ์ •์—์„œ AWS

zerone-code.tistory.com

 

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

Study Repository

rlaehddnd0422

ํ™œ๋™ํ•˜๊ธฐ