yoncho`s blog

1. Airflow (w/Docker) | Docker Container에 Airflow 구축 본문

기술, 나의 공부를 공유합니다./Apache Airflow

1. Airflow (w/Docker) | Docker Container에 Airflow 구축

욘초 2024. 6. 14. 19:56

앞 글의 내용처럼 Airflow는 Workflow 관리 툴이다 !! python 기반의 DAGs을 Scheduling & Monitoring 가능하며 Operator 종류도 다양해 ssh 접속, python 함수, shell 명령어 등의 동작 수행이 가능하다!

 

Airflow 구성을 위해선 가상 환경(venv, linux계열)이 필요하고
특히, 복잡한 Airflow 구성을 언제든 빠르게 타 PC로 이식할 수 있게 Docker Image와 Docker Compose 파일로 구성할 수 있다. 본 글에서 Window PC 환경에서 WSL에 Ubuntu-18.04에 Docker로 Airflow를 구동시켜보겠다.

 

 

#목차

1. 환경 및 필요 파일

2. Local <=> WSL(ubuntu-18.04) port forwarding 

3. WSL에 Ubuntu-18.04 설치

4. Docker 설치

5. Airflow Custom Docker Image 작업

6. Airflow Docker Compose yaml 파일 작업

7. Airflow Docker Container 빌드 후 Airflow 확인

8. 최종

 

 

[1] 환경 및 필요 파일

본 작업은 Windows 10 PC에서 WSL2 (Ubuntu-18.04)에 작업을 진행할 것이다.

기본적으로 Airflow로 실행시키려는 Work는 Oracle과 Postgre DB 접근을 필요로 하기 때문에 ojdbc8.jar와 postgre sql 관련 패키지들을 파일 혹은 설치 패키지로 명시해야한다.

추가로 본 작업의 기본 작업 디렉터리는 C:/User/<USER_NAME>/ 디렉터리로 하겠다.

**ojdbc8.jar는 첨부해놓겠다.

ojdbc8.jar
6.73MB

 

[2] Local <=> WSL(ubuntu-18.04) port forwarding 

이 작업은 추후 Clustering을 할 때 필요하지만 먼저 작업하려한다.

Window WSL기능으로 Linux계열을 사용할 수 있다. 

만약, 외부로부터 호스트 PC WSL에 설치된 Linux에 접근하려면 당연히 PC 방화벽 설정도 중요하지만

Local <=> WSL간의 port forwarding도 해줘야한다.

 

#Local <=> WSL간 port forwarding 실습

I. C:/User/<USER_NAME>/ 디렉터리 아래 ports_wsl.ps1 파일을 생성하고 아래와 같이 작업한

If (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
$arguments = "& '" + $myinvocation.mycommand.definition + "'"
Start-Process powershell -Verb runAs -ArgumentList $arguments
Break
}

$remoteport = bash.exe -c "ifconfig eth0 | grep 'inet '"
$found = $remoteport -match '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}';

if ( $found ) {
$remoteport = $matches[0];
}
else {
Write-Output "The Script Exited, the ip address of WSL 2 cannot be found";
exit;
}

$ports = @(3000, 3001, 5000, 5500, 19000, 19002, 19006); #원하는 port 번호

Invoke-Expression "netsh interface portproxy reset";

for ( $i = 0; $i -lt $ports.length; $i++ ) {
$port = $ports[$i];
Invoke-Expression "netsh interface portproxy add v4tov4 listenport=$port connectport=$port connectaddress=$remoteport";
}

Invoke-Expression "netsh interface portproxy show v4tov4";

 

II. 실행 시켜 포트포워딩을 적용 및 결과 확인

#명령어
PS C:\Users\joyon> .\ports_wsl.ps1

#결과
ipv4 수신 대기:             ipv4에 연결:

주소            포트        주소            포트
--------------- ----------  --------------- ----------
*               3000        xxx.xx.xxx.xxx  3000
*               3001        xxx.xx.xxx.xxx  3001
*               5000        xxx.xx.xxx.xxx  5000
*               5500        xxx.xx.xxx.xxx  5500
*               19000       xxx.xx.xxx.xxx  19000
*               19002       xxx.xx.xxx.xxx  19002
*               19006       xxx.xx.xxx.xxx  19006

PS C:\Users\joyon>

 

 

[3] WSL에 Ubuntu-18.04 설치

일반적인 PC라면 Microsoft Store에서 다운받을 수 있다. 

필자는 회사나 특정 공간에서 방화벽에 의해 Microsoft Store로 Ubuntu-18.04를 못 받는 상황을 가정하고 진행하겠다.

 

#Ubuntu-18.04 설치

I. 명령어 작업

#설치 명령어
curl.exe -k -L -o ubuntu-1804.appx https://aka.ms/wsl-ubuntu-1804

#방법 - 1
Add-AppxPackage .\ubuntu-1804.appx
작업 디렉터리로 이동 후 ubuntu-1804.appx 실행
=> 완료

#방법 - 2
Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip
Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804
작업 디렉터리 아래 ubuntu-1804/ 디렉터리 아래 실행 파일 .exe 실행
=> 완료

방법은 2가지이다. 만약 1번 방법이 안된다면 필자는 2번 방법을 제안한다..!

이거 한다고... 피똥 많이 쌈 ;ㅅ;

 

설치 완료 후 실행 시키면 기본적으로 Ubuntu-18.04에서 사용할 username과 password를 입력하라고 뜬다.

그러면 우리가 원했던 Ubuntu-18.04 설치는 완료된 것이다!!

 

[4] Docker 설치 (Ubuntu-18.04)

**이번 단계부터는 (Ubuntu-18.04)라고 표시된다. 방금 설치한 Ubuntu-18.04 환경에서 진행하겠다는 의미이다.

기본적으로 Ubuntu-18.04에는 우리한테 필요한 Docker라든지.. 필요 패키지가 없기 때문에 다 설치해줘야한다..!

 

#Docker 설치

I. 명령어 작업

#1
sudo apt-get update

#2 
sudo apt-get install ca-certificates curl gnupg -y

#3
sudo install -m 0755 -d /etc/apt/keyrings

#4
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

#5
sudo chmod a+r /etc/apt/keyrings/docker.gpg

#6
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

#7 설치 완료.
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

#8 Shell Reboot 후. 
sudo service docker start
>> * Starting Docker: docker    [ OK ]

#9 Docker Test
sudo docker run hello-world
>> Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
....

위 명령어를 #1 ~ #9 까지 진행하면 Docker 설치는 완료되었다.

 

[5] Airflow Custom Docker Image 작업 (Ubuntu-18.04)

이제 Airflow 실행을 위해 Docker Image를 만들어볼 것이다. 

airflow가 기본적으로 제공하는 이미지가 있지만, airflow가 설치될 때 같이 설치해야할 파일과 선행 작업들이 존재한다.

그래서 Airflow Custom Docker File을 만들 것이다.


#Airflow Custom Docker File 작업

I. Airflow 작업 디렉터리 생성

우선 Airflow 설치와 구동을 위한 작업 디렉터리를 만들겠다.

경로는 /home/<user_name>/ 디렉터리 아래에 airflow/ 디렉터리를 생성하겠다.

mkdir airflow/

 

II. airflow/ 디렉터리 아래 dockerfile 파일 생성 후 아래와 같이 작업한다.

#파일 생성
vi dockerfile

--- dockerfile 내용 ---
FROM apache/airflow:2.8.2
USER root
RUN apt-get update && apt-get install -y python3-pip

COPY requirements.txt /requirements.txt
COPY ojdbc8.jar /ojdbc8.jar

RUN curl -k -L -o OpenJDK8U-jdk_x64_linux_8u342b07.tar.gz https://github.com/AdoptOpenJDK/openjdk8-upstream-binaries/releases/download/jdk8u342-b07/OpenJDK8U-jdk_x64_linux_8u342b07.tar.gz
RUN mkdir -p /usr/lib/jvm
RUN tar zxvf OpenJDK8U-jdk_x64_linux_8u342b07.tar.gz -C /usr/lib/jvm
RUN echo -e '\nJAVA_HOME="/usr/lib/jvm/openjdk-8u342-b07"' >> /etc/environment
RUN source /etc/environment
RUN echo -e '\nexport JAVA_HOME=/usr/lib/jvm/openjdk-8u342-b07\nexport PATH="$PATH:$JAVA_HOME/bin"' >> ~/.bashrc
RUN source ~/.bashrc

USER airflow
RUN pip install --user --upgrade pip
RUN pip install --trusted-host pypi.org --trusted-host files.pythonhosted.org -r /requirements.txt

위 docker image를 간단히 설명하면 apache/airflow:2.8.2를 기반으로하여 

설치할 때 root 권한으로 필요 패키지 (python3-pip, JDK8u ..)를 설치하고 Local에서 필요 파일 (ojdbc8.jar, requirements.txt)을 복사해간다. 

그리고 파일과 패키지에 대한 환경 설정 적용을 진행한다.

그후 airflow 계정으로 사용자를 변경하고 requirements.txt에 할당된 python 패키지를 다운 받는다.

 

III. dockerfile 실행 해 docker-image 만들기

sudo docker build -t yoncho_airflow_0.0.1 .

위 명령어는 현재 작업 디렉터리 ( dockerfile이 위치한 경로 )에서 수행해야하며 

만약 다른 곳에서 실행할 경우 맨 끝 '.' 을 dockerfile이 위치한 절대 경로를 입력해야한다.

 

위 명령어가 정상적으로 수행되면 yoncho-airflow:0.0.1이라는 Docker Image가 Docker에 등록된다.

Docker Image를 확인해보려면 아래 명령어를 수행하자.

#명령어
sudo docker images

#결과
REPOSITORY             TAG       IMAGE ID       CREATED         SIZE
yoncho_airflow_0.0.1   latest    cee3e2c62762   6 days ago      2.25GB
...

 

 

[6] Airflow Docker Compose yaml 파일 작업 (Ubuntu-18.04)

이제 우리가 만든 Airflow Image를 가지고 Airflow를 실행 시킬 Docker Container를 만들어보자.

Docker 작업을 한번이라도 해본 적 있다면 docker-compose.yaml 파일을 알 것이다.

Docker Container를 생성해주는 Script이고 그 안에 여러 정보들을 명시해놓을 수 있다.

 

#Airflow Docker Compose yaml 파일 작업

I. 기본적인 Airflow 폴더 구성을 위해 logs/ , dags/ , plugins/ , config/ 디렉터리를 생성 

airflow/
|--requirements.txt		<- 필요 패키지 목록
|--ojdbc8.jar			<- oracle 패키지
|--dockerfile			<- docker image 생성 파일
|--docker-compose.yaml
|--logs/
|--dags/
|--plugins/
|--config/

 

II. airflow/ 디렉터리 아래 docker-compose.yaml 파일 생성 및 아래와 같이 작업한다.

x-airflow-common:
  &airflow-common
  # In order to add custom dependencies or upgrade provider packages you can use your extended image.
  # Comment the image line, place your Dockerfile in the directory where you placed the docker-compose.yaml
  # and uncomment the "build" line below, Then run `docker-compose build` to build the images.
  image: yoncho_airflow_0.0.1
  # build: .
  environment:
    &airflow-common-env
    AIRFLOW__CORE__EXECUTOR: CeleryExecutor
    AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow
    AIRFLOW__CELERY__RESULT_BACKEND: db+postgresql://airflow:airflow@postgres/airflow
    AIRFLOW__CELERY__BROKER_URL: redis://:@redis:6379/0
    AIRFLOW__CORE__FERNET_KEY: ''
    AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION: 'true'
    AIRFLOW__CORE__LOAD_EXAMPLES: 'true'
    AIRFLOW__API__AUTH_BACKENDS: 'airflow.api.auth.backend.basic_auth,airflow.api.auth.backend.session'
    # yamllint disable rule:line-length
    # Use simple http server on scheduler for health checks
    # See https://airflow.apache.org/docs/apache-airflow/stable/administration-and-deployment/logging-monitoring/check-health.html#scheduler-health-check-server
    # yamllint enable rule:line-length
    AIRFLOW__SCHEDULER__ENABLE_HEALTH_CHECK: 'true'
    # WARNING: Use _PIP_ADDITIONAL_REQUIREMENTS option ONLY for a quick checks
    # for other purpose (development, test and especially production usage) build/extend Airflow image.
    _PIP_ADDITIONAL_REQUIREMENTS: ${_PIP_ADDITIONAL_REQUIREMENTS:-}
  volumes:
    - ${AIRFLOW_PROJ_DIR:-.}/dags:/opt/airflow/dags
    - ${AIRFLOW_PROJ_DIR:-.}/logs:/opt/airflow/logs
    - ${AIRFLOW_PROJ_DIR:-.}/config:/opt/airflow/config
    - ${AIRFLOW_PROJ_DIR:-.}/plugins:/opt/airflow/plugins
  user: "${AIRFLOW_UID:-50000}:0"
  depends_on:
    &airflow-common-depends-on
    redis:
      condition: service_healthy
    postgres:
      condition: service_healthy

services:
  postgres:
    image: postgres:13
    environment:
      POSTGRES_USER: airflow
      POSTGRES_PASSWORD: airflow
      POSTGRES_DB: airflow
    ports:
      - "5432:5432"
    volumes:
      - postgres-db-volume:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "airflow"]
      interval: 10s
      retries: 5
      start_period: 5s
    restart: always

  redis:
    image: redis:latest
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 30s
      retries: 50
      start_period: 30s
    restart: always

  airflow-webserver:
    <<: *airflow-common
    command: webserver
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    restart: always
    depends_on:
      <<: *airflow-common-depends-on
      airflow-init:
        condition: service_completed_successfully

  airflow-scheduler:
    <<: *airflow-common
    command: scheduler
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost:8974/health"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    restart: always
    depends_on:
      <<: *airflow-common-depends-on
      airflow-init:
        condition: service_completed_successfully

  airflow-worker:
    <<: *airflow-common
    command: celery worker
    healthcheck:
      # yamllint disable rule:line-length
      test:
        - "CMD-SHELL"
        - 'celery --app airflow.providers.celery.executors.celery_executor.app inspect ping -d "celery@$${HOSTNAME}" || celery --app airflow.executors.celery_executor.app inspect ping -d "celery@$${HOSTNAME}"'
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    environment:
      <<: *airflow-common-env
      # Required to handle warm shutdown of the celery workers properly
      # See https://airflow.apache.org/docs/docker-stack/entrypoint.html#signal-propagation
      DUMB_INIT_SETSID: "0"
    restart: always
    depends_on:
      <<: *airflow-common-depends-on
      airflow-init:
        condition: service_completed_successfully

  airflow-triggerer:
    <<: *airflow-common
    command: triggerer
    healthcheck:
      test: ["CMD-SHELL", 'airflow jobs check --job-type TriggererJob --hostname "$${HOSTNAME}"']
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    restart: always
    depends_on:
      <<: *airflow-common-depends-on
      airflow-init:
        condition: service_completed_successfully

  airflow-init:
    <<: *airflow-common
    entrypoint: /bin/bash
    # yamllint disable rule:line-length
    command:
      - -c
      - |
        if [[ -z "${AIRFLOW_UID}" ]]; then
          echo
          echo -e "\033[1;33mWARNING!!!: AIRFLOW_UID not set!\e[0m"
          echo "If you are on Linux, you SHOULD follow the instructions below to set "
          echo "AIRFLOW_UID environment variable, otherwise files will be owned by root."
          echo "For other operating systems you can get rid of the warning with manually created .env file:"
          echo "    See: https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html#setting-the-right-airflow-user"
          echo
        fi
        one_meg=1048576
        mem_available=$$(($$(getconf _PHYS_PAGES) * $$(getconf PAGE_SIZE) / one_meg))
        cpus_available=$$(grep -cE 'cpu[0-9]+' /proc/stat)
        disk_available=$$(df / | tail -1 | awk '{print $$4}')
        warning_resources="false"
        if (( mem_available < 4000 )) ; then
          echo
          echo -e "\033[1;33mWARNING!!!: Not enough memory available for Docker.\e[0m"
          echo "At least 4GB of memory required. You have $$(numfmt --to iec $$((mem_available * one_meg)))"
          echo
          warning_resources="true"
        fi
        if (( cpus_available < 2 )); then
          echo
          echo -e "\033[1;33mWARNING!!!: Not enough CPUS available for Docker.\e[0m"
          echo "At least 2 CPUs recommended. You have $${cpus_available}"
          echo
          warning_resources="true"
        fi
        if (( disk_available < one_meg * 10 )); then
          echo
          echo -e "\033[1;33mWARNING!!!: Not enough Disk space available for Docker.\e[0m"
          echo "At least 10 GBs recommended. You have $$(numfmt --to iec $$((disk_available * 1024 )))"
          echo
          warning_resources="true"
        fi
        if [[ $${warning_resources} == "true" ]]; then
          echo
          echo -e "\033[1;33mWARNING!!!: You have not enough resources to run Airflow (see above)!\e[0m"
          echo "Please follow the instructions to increase amount of resources available:"
          echo "   https://airflow.apache.org/docs/apache-airflow/stable/howto/docker-compose/index.html#before-you-begin"
          echo
        fi
        mkdir -p /sources/logs /sources/dags /sources/plugins
        chown -R "${AIRFLOW_UID}:0" /sources/{logs,dags,plugins}
        exec /entrypoint airflow version
    # yamllint enable rule:line-length
    environment:
      <<: *airflow-common-env
      _AIRFLOW_DB_MIGRATE: 'true'
      _AIRFLOW_WWW_USER_CREATE: 'true'
      _AIRFLOW_WWW_USER_USERNAME: ${_AIRFLOW_WWW_USER_USERNAME:-airflow}
      _AIRFLOW_WWW_USER_PASSWORD: ${_AIRFLOW_WWW_USER_PASSWORD:-airflow}
      _PIP_ADDITIONAL_REQUIREMENTS: ''
    user: "0:0"
    volumes:
      - ${AIRFLOW_PROJ_DIR:-.}:/sources

  airflow-cli:
    <<: *airflow-common
    profiles:
      - debug
    environment:
      <<: *airflow-common-env
      CONNECTION_CHECK_MAX_COUNT: "0"
    # Workaround for entrypoint issue. See: https://github.com/apache/airflow/issues/16252
    command:
      - bash
      - -c
      - airflow

  # You can enable flower by adding "--profile flower" option e.g. docker-compose --profile flower up
  # or by explicitly targeted on the command line e.g. docker-compose up flower.
  # See: https://docs.docker.com/compose/profiles/
  flower:
    <<: *airflow-common
    command: celery flower
    profiles:
      - flower
    ports:
      - "5555:5555"
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost:5555/"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
    restart: always
    depends_on:
      <<: *airflow-common-depends-on
      airflow-init:
        condition: service_completed_successfully

volumes:
  postgres-db-volume:

위 명령어는 airflow 공식 홈페이지에서 제공하는 기본적인 docker-compose.yaml에서 조금의 수정을 진행했다.

image: yoncho_airflow_0.0.1은 우리가 앞서 만든 Airflow Custom Docker Image를 Container Image로 사용하겠다는 뜻이다. service 목록 中 postgre, redis에 ports 정보를 별도로 표시해두었다.

 

 

III. airflow UID 등록

우리가 만들 airflow의 user id를 등록하기 위해 UID를 .env 파일에 작업해주자.

echo -e "AIRFLOW_UID=$(id -u)" > .env
#결과 확인
cat .env
>> AIRFLOW_UID=1000

위 단계를 끝으로 우리는 Airflow Custom Docker Image로 Docker Container를 구동시킬 준비가 끝났다.

 

 

7. Airflow Docker Container 빌드 후 Airflow 확인 (Ubuntu-18.04)

앞에서 준비가 다 끝났으면 남은 일은 하나!

실행시켜보는 것이다.

 

#Airflow 실행

I. 실행 명령어

#초기화
sudo docker compose up airflow-init

#실행
sudo docker compose up

#결과
airflow-master-airflow-triggerer-1  |   ____________       _____________
airflow-master-airflow-triggerer-1  |  ____    |__( )_________  __/__  /________      __
airflow-master-airflow-triggerer-1  | ____  /| |_  /__  ___/_  /_ __  /_  __ \_ | /| / /
airflow-master-airflow-triggerer-1  | ___  ___ |  / _  /   _  __/ _  / / /_/ /_ |/ |/ /
airflow-master-airflow-triggerer-1  |  _/_/  |_/_/  /_/    /_/    /_/  \____/____/|__/
airflow-master-airflow-triggerer-1  | [2024-06-29 03:17:48 +0000] [16] [INFO] Starting gunicorn 21.2.0
airflow-master-airflow-triggerer-1  | [2024-06-29 03:17:48 +0000] [16] [INFO] Listening at: http://[::]:8794 (16)
airflow-master-airflow-triggerer-1  | [2024-06-29 03:17:48 +0000] [16] [INFO] Using worker: sync
airflow-master-airflow-triggerer-1  | [2024-06-29 03:17:48 +0000] [17] [INFO] Booting worker with pid: 17
airflow-master-airflow-triggerer-1  | [2024-06-29 03:17:48 +0000] [18] [INFO] Booting worker with pid: 18
airflow-master-airflow-triggerer-1  | [2024-06-29T03:17:48.847+0000] {triggerer_job_runner.py:174} INFO - Setting u

 

위 명령어 수행 처럼 Airflow 로고가 표시되고 Airflow의 구성 (Webserver, Worker, Scheduler, Trigger 등)의 실행 로그가 표시될 것이다. 에러 없이 로그가 출력되고 있다면 localhost:8080/ 을 크롬창에 입력해 들어가보자

 

II. GUI Tool 확인

http://localhost:8080/ 입력 시 로그인창이 표시된다.

Username/Password 기본값은 'airflow' 이다.

로그인하게되면 Airflow가 제공해주는 Management Tool을 활용해 DAGs Scheduling과 Monitoring 

그리고 상세 로그 등을 확인할 수 있다.

 

 

 

[8] 최종

이번 글에서는 Airflow를 Docker Container에 올리는 실습을 진행했다. 

코드 상의 내용은 한번 직접 진행하면서 알아보면 더 좋을 듯 하다.

 

docker-compose.yaml 파일 내용을 읽다보면 AIRFLOW__CORE__LOAD_EXAMPLES = 'true' 로 되어있을 것이다.

해당 변수는 기본적으로 Airflow가 제공해주는 DAG를 등록하겠다는 뜻으로 Management (localhost:8080) 에서 그 목록을 확인할 수 있다.

 

최종적으로 우린 아래 그림과 같은 계층으로 Airflow를 실행시키고있다.



Comments