├── .gitignore ├── 0.commands ├── README.md └── tips.bash ├── 1.hello ├── Alpine.dockerfile ├── Dockerfile ├── README.md └── Sample.dockerfile ├── 10.dockercompose ├── README.md ├── demo1_wordpress │ ├── docker-compose-wordpress-advanced1.yml │ ├── docker-compose-wordpress-advanced2.yml │ └── docker-compose.yml ├── demo2_myapp │ ├── docker-compose.yml │ └── nginx.conf ├── docker-compose-eg1.yml ├── docker-compose-eg2.yml ├── docker-compose-eg3.yml └── docker-compose.yml ├── 11.exercise ├── README.txt ├── a.reverse-proxy-local │ ├── service1 │ ├── service2 │ ├── service3 │ └── service4 ├── b.reverse-proxy-docker │ ├── Dockerfile │ └── nginx │ │ └── conf │ │ ├── default.conf │ │ └── proxy_params └── c.reverse-proxy-dockercompose │ ├── docker-compose.yaml │ └── nginx │ └── conf │ ├── default.conf │ └── proxy_params ├── 12.buildx └── README.md ├── 2.nginx ├── Dockerfile └── README.md ├── 20.scratch ├── README.txt ├── main.go ├── main1.go ├── main2.go ├── main3.go ├── main4.go ├── main5.go ├── main6.go ├── main7.go ├── main8.go └── main9.go ├── 21.api └── README.md ├── 22.security ├── README.md ├── express_docker_security.md └── flask_docker_security.md ├── 23.cloud ├── 1..setup-aws-ecr │ ├── 1.awscli.txt │ ├── 2.ecrecs.txt │ └── 3.cicd.txt ├── 10.billing │ └── aws-cost-usage.py ├── 9.cleanup-aws │ ├── delete_all_ec2.py │ ├── delete_all_ecr.py │ └── delete_all_ecs.py └── README.md ├── 3.flask ├── .dockerignore ├── Dockerfile └── app.py ├── 4.flask ├── .dockerignore ├── Dockerfile ├── app.py ├── requirements.txt └── templates │ └── hello.html ├── 5.express ├── .dockerignore ├── Dockerfile ├── README.txt ├── app.js ├── package-lock.json └── package.json ├── 6.express ├── .dockerignore ├── .env.template ├── Dockerfile ├── app.js ├── package.json └── views │ └── hello.html ├── 7.java ├── Dockerfile ├── Dockerfile.v2 ├── Dockerfile.v3 ├── Dockerfile.v4 ├── HelloWorld.java └── README.txt ├── 8.golang ├── Dockerfile ├── Dockerfile.v2 ├── Dockerfile.v3 ├── README.txt └── main.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /0.commands/README.md: -------------------------------------------------------------------------------- 1 | # 도커 개요 2 | 3 | ## 도커 배경 4 | - 생략 (수업 참고) 5 | 6 | ## 도커 설치 7 | - 도커 설치 환경 8 | - Ubuntu 20.04 (VirtualBox) 9 | - 설치 방법 10 | - 방법1 (추천) : ` sudo apt install docker.io ` 11 | - 방법2 (비추천) : ` curl -fsSL https://get.docker.com | sudo sh ` 12 | - 권한 부여 13 | - 사용자 권한 : ` sudo usermod -aG docker ${USER} && newgrp docker ` 14 | - 도커 디렉토리 변경 15 | - ` docker info | grep -i "Root" ` 16 | - 방법1 : (비추천) 도커 버전 업데이트 시 시스템 설정 파일 overwrite 될 수 있음 17 | - ` sudo vi /lib/systemd/system/docker.service ` 18 | - ` ExecStart= (중략) --data-root=/data/docker_dir ` 19 | - ` sudo systemctl daemon-reload ` 20 | - ` sudo systemctl restart docker ` 21 | - 방법2 : (추천) 도커 버전 업데이트와 무관하게 동작 22 | - ` sudo vi /etc/docker/daemon.json ` 23 | ```json 24 | { 25 | "data-root":"/data/docker_dir" 26 | } 27 | ``` 28 | 29 | ## 도커 이미지 30 | - 퍼블릭 도커 허브 31 | - 경로 : https://hub.docker.com 32 | 33 | ## 실습 34 | ### 1. Hello, Docker 35 | - 명령: ` docker run hello-world ` 36 | - 명령: ` docker run docker/whalesay cowsay Hello World! ` 37 | 38 | ### 2. 기본 명령어 39 | - ` docker ps ` 40 | - ` docker ps -a ` 41 | - ` docker container ls ` 42 | - ` docker container ls -a ` 43 | - ` docker images ` 44 | 45 | ### 3. 커널 46 | - 이미지 풀 47 | - ` docker pull ubuntu:14.04 ` 48 | - ` docker pull ubuntu:16.04 ` 49 | - ` docker pull ubuntu:18.04 ` 50 | - 이미지 실행 및 버전 확인 51 | - ` docker run -it ubuntu:14.04 bash ` 52 | ```bash 53 | uname -a 54 | cat /etc/*-release 55 | ``` 56 | 57 | ### 4. 실행, 접속, 종료 외 58 | - 생성, 실행, 및 접속 59 | - ` docker create xxx ` 60 | - ` docker start xxx ` 61 | - ` docker attach xxx ` 62 | - 종료, 삭제 (컨테이너) 63 | - ` docker stop xxx ` 64 | - ` docker rm xxx ` 65 | - ` docker rm -f xxx ` 66 | - ` docker rm $(docker ps -aq) ` 67 | - 삭제 (이미지) 68 | - ` docker rmi xxx ` 69 | - ` docker rmi $(docker images -q) ` 70 | 71 | ### 5. 실습 예제 72 | - 실습1. 프로세스, 접속 73 | - ` docker run ubuntu sleep 100 ` 74 | - ` docker ps -a ` 75 | - ` docker exec cat /etc/hosts ` 76 | - 실습2. 포트 바인딩 77 | - ` docker run nginx ` 78 | - ` docker run --name mynginx -p 80:80 -d nginx ` 79 | - 실습3. 포트 바인딩 80 | - ` docker run redis ` 81 | - ` docker run -d -p 6379:6379 redis ` 82 | - ```bash 83 | telnet localhost 6379 84 | set hello world 85 | get hello 86 | ``` 87 | - 실습4. 환경변수, 디렉토리 바인딩(호스트패스, 볼륨) 88 | - ` docker run --name mysqldb -d -p 3306:3306 mysql:5.7 ` 89 | - ` docker logs mysqldb ` 90 | - ` docker run --name mysqldb -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql:5.7 ` 91 | - ` docker logs -f xxxx ` 92 | - ` mysql -h127.0.0.1 -uroot ` 93 | - 호스트패스 바인딩 : ` docker run -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=true --name mysqldb -v /data/database:/var/lib/mysql mysql:5.7 ` 94 | - 볼륨 생성 : ` docker volume create mysql_volume ` 95 | - 볼륨 바인딩 : ` docker run -d -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=true --name mysqldb -v mysql_volume:/var/lib/mysql mysql:5.7 ` 96 | - 샘플 query문 97 | ```bash 98 | SHOW DATABASES; 99 | CREATE DATABASE soma; 100 | USE soma; 101 | CREATE TABLE users(id INT NOT NULL AUTO_INCREMENT, name VARCHAR(20), PRIMARY KEY(id)); 102 | INSERT INTO users(name) VALUES("hello"); 103 | INSERT INTO users(name) VALUES("world"); 104 | SELECT * FROM users; 105 | ``` 106 | - 실습5. 네트워크 107 | - ` docker inspect xxx ` 108 | - ` docker inspect xxx | grep "IPAddress" ` 109 | - ` docker network create --driver bridge --subnet 192.168.222.0/24 my-network ` 110 | - ` docker run ubuntu:16.04 --network=my-network bash ` 111 | ```bash 112 | apt update 113 | apt install net-tools -y 114 | apt install iputils-ping 115 | ``` 116 | - ` docker network ls ` 117 | - ` docker network prune ` 118 | - 심화 exercise 119 | ```bash 120 | docker network create --driver bridge --subnet 192.168.1.0/24 my-test-network1 121 | docker network create --driver bridge --subnet 192.168.2.0/24 my-test-network2 122 | docker network ls 123 | 124 | docker run -it --name myos1 --network my-test-network1 ubuntu:20.04 125 | docker run -it --name myos2 --network my-test-network1 ubuntu:20.04 126 | 127 | docker run -it --name myos3 --network my-test-network2 ubuntu:20.04 128 | docker run -it --name myos4 --network my-test-network2 ubuntu:20.04 129 | 130 | ping 192.168.1.2 131 | ping 192.168.1.3 132 | ping 192.168.2.1 133 | ping 192.168.2.2 134 | 135 | ping myos1 136 | ping myos2 137 | ping myos3 138 | ping myos4 139 | ``` 140 | - 실습6. 도커간 연동 (워드프레스 & DB) - link 로 연결 141 | - ` docker run -d -p 8080:80 --link mysql:mydatabase -e WORDPRESS_DB_HOST=mydatabase -e WORDPRESS_DB_NAME=wp -e WORDPRESS_DB_USER=wp -e WORDPRESS_DB_PASSWORD=wp wordpress ` 142 | - MYSQL 5.7 143 | ```bash 144 | mysql -h127.0.0.1 -uroot 145 | create database wp; 146 | grant all privileges on wp.* to wp@'%' identified by 'wp'; 147 | flush privileges; 148 | quit 149 | ``` 150 | - MYSQL 8.0 (개별 사용자 생성 필요) 151 | ``` bash 152 | mysql -h127.0.0.1 -uroot 153 | create database wp; 154 | create user wp@'%' identified by 'wp'; 155 | grant all privileges on wp.* to wp@'%' with grant option; 156 | flush privileges; 157 | quit 158 | ``` 159 | - 실습 6-2. 도커간 연동 (워드프레스 & DB) - network 로 연결 160 | - 두 컨테이너 모두 같은 네트워크에 배치 (서로 컨테이너 이름으로 호스트 접근 가능) 161 | ``` bash 162 | docker run --name mysql -d -p 3306:3306 --network my-network -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql:5.7 163 | 164 | docker run -d -p 8080:80 --network my-network -e WORDPRESS_DB_HOST=mysql -e WORDPRESS_DB_NAME=wp -e WORDPRESS_DB_USER=wp -e WORDPRESS_DB_PASSWORD=wp wordpress 165 | ``` 166 | - 실습7. 컨테이너 이미지 저장 (vim 설치, main 변경 및 이미지 저장) 167 | - ` docker run --name my-nginx -p 80:80 -d nginx ` 168 | - ` docker exec -it my-nginx bash ` 169 | ```bash 170 | apt update 171 | apt install vim 172 | vi /usr/share/nginx/index.html 173 | ``` 174 | - ` docker commit -m 'nginx + vim' my-nginx my-nginx:1.0 ` 175 | - ` docker history my-nginx:1.0 ` 176 | -------------------------------------------------------------------------------- /0.commands/tips.bash: -------------------------------------------------------------------------------- 1 | function docker-taglist { 2 | if [ ! -z $1 ]; then 3 | curl -s https://registry.hub.docker.com/v1/repositories/$1/tags | sed "s/,/\n/g" | grep name | cut -d '"' -f 4 4 | # curl -s https://hub.docker.com/v2/repositories/$1/tags/list | jq 5 | else 6 | echo -e "\nusage: docker-taglist [imagename]\n" 7 | fi 8 | } 9 | 10 | function docker-container-ip { 11 | if [ ! -z $1 ]; then 12 | if [ ! -z $2 ]; then 13 | NET_NAME="\"$2\"" 14 | else 15 | NET_NAME="bridge" 16 | fi 17 | docker inspect $1 | jq -r ".[0].NetworkSettings.Networks.${NET_NAME}.IPAddress" 18 | else 19 | echo -e "\nusage: docker-container-ip [container-name] [container-network]\n" 20 | fi 21 | } 22 | 23 | function docker-taglist2 () { 24 | local repo=${1} 25 | local page_size=${2:-100} 26 | [ -z "${repo}" ] && echo "Usage: listTags [page_size]" 1>&2 && return 1 27 | local base_url="https://registry.hub.docker.com/api/content/v1/repositories/public/library/${repo}/tags" 28 | 29 | local page=1 30 | local res=$(curl "${base_url}?page_size=${page_size}&page=${page}" 2>/dev/null) 31 | local tags=$(echo ${res} | jq --raw-output '.results[].name') 32 | local all_tags="${tags}" 33 | 34 | local tag_count=$(echo ${res} | jq '.count') 35 | 36 | ((page_count=(${tag_count}+${page_size}-1)/${page_size})) # ceil(tag_count / page_size) 37 | 38 | for page in $(seq 2 $page_count); do 39 | tags=$(curl "${base_url}?page_size=${page_size}&page=${page}" 2>/dev/null | jq --raw-output '.results[].name') 40 | all_tags="${all_tags}${tags}" 41 | done 42 | 43 | echo "${all_tags}" | sort 44 | } 45 | -------------------------------------------------------------------------------- /1.hello/Alpine.dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | ENTRYPOINT ["echo", "hello"] 4 | 5 | -------------------------------------------------------------------------------- /1.hello/Dockerfile: -------------------------------------------------------------------------------- 1 | # My Base Image 2 | FROM ubuntu 3 | 4 | # My cmd... 5 | CMD ["echo", "hello, docker"] 6 | -------------------------------------------------------------------------------- /1.hello/README.md: -------------------------------------------------------------------------------- 1 | # 이미지 빌드 2 | docker build . 3 | docker build --tag myhello:0.1 . 4 | 5 | 6 | # 빌드 현황 모니터링 7 | docker build --progress=plain . 8 | 9 | 10 | # 이전 캐쉬 사용하지 않고 빌드 11 | docker build --no-cache . 12 | 13 | -------------------------------------------------------------------------------- /1.hello/Sample.dockerfile: -------------------------------------------------------------------------------- 1 | # Full Docs : https://docs.docker.com/engine/reference/builder/ 2 | 3 | # FROM 이미지:태그 - 태그 기본값은 latest 4 | FROM ubuntu:20.04 5 | 6 | # 제작자 - 이 또한 하나의 layer로 생성됨 7 | MAINTAINER shpark 8 | 9 | # MAINTAINER 는 deprecated 되고, LABEL 을 사용해서 정의 10 | # LABEL = = ... 11 | LABEL org.opencontainers.image.authors="shpark@emailaddress" 12 | LABEL version="1.0" \ 13 | description="This is my app" 14 | 15 | # 해당 컨테이너 안에서 실행할 명령어 16 | # RUN 하나당 하나의 layer 생성. 불필요한 layer 줄이기 위해서는 && \ 사용해서 하나의 명령어로 변경 17 | RUN apt-get update 18 | RUN apt-get install nginx -y && \ 19 | echo "\ndaemon off;" >> /etc/nginx/nginx.conf 20 | 21 | # 시작 디렉토리 정의 22 | WORKDIR /data/myapp 23 | 24 | # 컨테이너 안으로 파일 복사 25 | # ADD 명령어 사용 시 복사와 함께 풀려서 들어감 - 또한 외부 URL 참조 시 해당 URL 풀어서 들어감 26 | # COPY 명령어 사용 시 원본 그대로 복사 됨 27 | ADD myapp.tar.gz /data/myapp 28 | COPY myapp.tar.gz /data/myapp 29 | 30 | # 컨테이너 시작 시 출발점 (명령어) 31 | # CMD 는 컨테이너 실행 시 param 으로 대체됨. Entrypoint 는 대체되지 않음 32 | # 각 명령어의 파라미터는 개별 인자로 입력 33 | ENTRYPOINT ["python", "app.py"] 34 | CMD ["echo", "hello, world"] 35 | CMD ["ls", "-al"] 36 | CMD ["ls", "-a", "-l"] 37 | 38 | # 엔트리 포인트와 CMD 혼용해서 사용또한 가능 함 39 | # 단 한 도커파일 내에 하나의 ENTRYPOINT 와 하나의 CMD 만 있어야 함 40 | ENTRYPOINT ["node"] 41 | CMD ["index.js"] 42 | 43 | # 컨테이너 내부 파일/디렉토리를 외부와 연결할 수 있음 44 | VOLUME ["/var/lib/mysql"] 45 | 46 | # 컨테이너 내부 포트를 외부에서 연결할 수 있음 - 이것과 무관하게 컨테이너 실행 시 -p 명령어 사용 필수 47 | # 기본적으로는 tcp 48 | EXPOSE 5000 49 | EXPOSE 80/tcp 50 | EXPOSE 80/udp 51 | 52 | # 컨테이너 내부 실행 시 사용자 계정/권한 - 생략 시 root 53 | USER www-data 54 | -------------------------------------------------------------------------------- /10.dockercompose/README.md: -------------------------------------------------------------------------------- 1 | # 도커 컴포즈 개요 2 | 3 | ## 도커 컴포즈 배경 4 | - 생략 (수업 참고) 5 | 6 | ## 도커 설치 7 | - 도커 설치 환경 8 | - Ubuntu 20.04 (VirtualBox) 9 | - 설치 방법 10 | - 방법1 (추천) : ` sudo apt install docker-compose ` 11 | 12 | ## 도커 컴포즈 문법 13 | - https://docs.docker.com/compose/compose-file/ 14 | - https://docs.docker.com/compose/compose-file/compose-file-v3/ 15 | - https://docs.docker.com/compose/compose-file/compose-file-v2/ 16 | 17 | ## 실습 18 | ### 1. 기본 명령어 19 | - 생성/종료(삭제)/중지/시작 20 | - ` docker-compose up ` 21 | - ` docker-compose up -d ` 22 | - ` docker-compose down ` 23 | - ` docker-compose stop ` 24 | - ` docker-compose start ` 25 | 26 | ### 2. 상세 명령어 27 | - 특정 파일명으로 만들어진 컴포즈 파일 실행 28 | - ` docker-compose -f myapp.yml -f myapp2.yml up -d ` 29 | - 프로세스 상태 보기 (내가 관리하는 컴포즈 파일 내의 컨테이너) 30 | - ` docker-compose ps ` 31 | - 설정 파일 보기 32 | - ` docker-compose config ` 33 | - 프로세스 로그 보기 (내가 관리하는 컴포즈 파일 내의 컨테이너) 34 | - ` docker-compose logs -f web ` 35 | - 특정 서비스(컨테이너) 내의 명령어 실행 (별도 컨테이너로 생성 됨) 36 | - ` docker-compose run web env ` 37 | - 현재 서비스(컨테이너) 내의 명령어 실행 (동작중인 컨테이너에서 명령 실행) 38 | - ` docker-compose exec db psql postgres postgres ` 39 | 40 | ### 3. 실습 예제 41 | - 플라스크 앱 도커 컴포즈로 생성하기 42 | - ` docker-dompose-eg1.yml ` 43 | - 플라스크 앱 및 SQL 컨테이너 생성하기 44 | - ` docker-dompose-eg2.yml ` 45 | - 플라스크 앱 및 SQL 컨테이너 생성하기 (다양한 설정 - 볼륨, 네트워크, 연관성) 46 | - ` docker-dompose-eg3.yml ` 47 | - https://docs.docker.com/compose/networking/ 48 | - https://docs.docker.com/compose/startup-order/ 49 | 50 | ### 4. 실제 예제 51 | - 워드프레스/MySQL 예제 : demo1_wordpress 52 | - 설정파일 리뷰 및 실행 53 | - ` docker-compose up -d ` 54 | - 필요 시 DB 계정 생성 55 | - ` mysql -u root -p ` 56 | ```bash 57 | create database wordpress; 58 | create user wp-user@'10.0.%' identified by ''; 59 | grant all privileges on wordpress.* to wp-user@'10.0.%' identified by '' with grant option; 60 | flush privileges; 61 | ``` 62 | - 권한 조회 63 | - ` show grants for my-user@'10.0.%' ` 64 | - 권한 삭제 65 | - ` revoke all on wordpress from wp-user@'10.0.%' ` 66 | - 워드프레스/MySQL/phpMyAdmin 예제 : demo1_wordpress 67 | - 환경변수 필요 시 생성 (.env) 68 | ```bash 69 | MYSQL_ROOT_PASSWORD=this-is-my-password 70 | MYSQL_USER=wp-user 71 | MYSQL_USER_PASWORD=this-is-my-password 72 | ``` 73 | - 설정파일 리뷰 및 실행 74 | - ` docker-compose up -d -f .env ` 75 | -------------------------------------------------------------------------------- /10.dockercompose/demo1_wordpress/docker-compose-wordpress-advanced1.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: mysql:5.7 # mysql:latest 도 무방 6 | volumes: 7 | - mysql_vol:/var/lib/mysql # 볼륨 미사용시 ./mysql_data 8 | restart: always # unless-stopped 를 권장 9 | environment: 10 | MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD 11 | MYSQL_USER: $MYSQL_USER # 사용자 계정 환경변수 12 | MYSQL_PASSWORD: $MYSQL_USER_PASSWORD # 사용자 암호 환경변수 13 | MYSQL_DATABASE: wordpress # DB 이름 14 | networks: 15 | - app-network # 필요한 경우 16 | 17 | wordpress: 18 | depends_on: 19 | - db 20 | image: wordpress:latest 21 | ports: 22 | - "80:80" 23 | restart: always # unless-stopped 를 권장 24 | environment: 25 | WORDPRESS_DB_HOST: db:3306 # 기본포트라 그냥 db만 써주어도 무방 26 | WORDPRESS_DB_USER: $MYSQL_UESR # 사용자 계정 환경변수 27 | WORDPRESS_DB_PASSWORD: $MYSQL_USER_PSSSWORD # 사용자 암호 환경변수 28 | WORDPRESS_DB_NAME: wordpress # DB 이름 29 | volumes: 30 | - wordpress_vol:/var/www/html # 볼륨 미사용시 ./wordpress_data 31 | # - ./php/php.ini:/usr/local/etc/php/php.ini # 필요한 경우 32 | # - ./upload.ini:/usr/local/etc/php/conf.d/uploads.ini # 필요한 경우 33 | networks: 34 | - app-network # 필요한 경우 35 | 36 | phpmyadmin: 37 | depends_on: 38 | - db 39 | image: phpmyadmin/phpmyadmin:latest 40 | env_file: .env 41 | environment: 42 | PMA_HOST: db 43 | MYSQL_ROOT_PASSWORD: $MYSQL_ROOT_PASSWORD 44 | ports: 45 | - 8080:80 46 | 47 | volumes: 48 | mysql_vol: 49 | wordpress_vol: 50 | 51 | networks: 52 | app-network: 53 | driver: bridge 54 | -------------------------------------------------------------------------------- /10.dockercompose/demo1_wordpress/docker-compose-wordpress-advanced2.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: mariadb:latest 6 | container_name: db 7 | restart: unless-stopped 8 | env_file: .env 9 | environment: 10 | - MYSQL_DATABASE=wordpress 11 | volumes: 12 | - ./dbdata:/var/lib/mysql 13 | networks: 14 | - app-network 15 | 16 | wordpress: 17 | depends_on: 18 | - db 19 | image: wordpress:fpm-alpine 20 | container_name: wordpress 21 | restart: unless-stopped 22 | env_file: .env 23 | environment: 24 | - WORDPRESS_DB_HOST=db:3306 25 | - WORDPRESS_DB_USER=$MYSQL_USER 26 | - WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORD 27 | - WORDPRESS_DB_NAME=wordpress 28 | volumes: 29 | - ./wordpress:/var/www/html 30 | - ./php/php.ini:/usr/local/etc/php/php.ini 31 | networks: 32 | - app-network 33 | 34 | webserver: 35 | depends_on: 36 | - wordpress 37 | image: nginx:alpine 38 | container_name: webserver 39 | restart: unless-stopped 40 | ports: 41 | - "80:80" 42 | - "443:443" 43 | volumes: 44 | - ./wordpress:/var/www/html 45 | - ./nginx-conf:/etc/nginx/conf.d 46 | - ./certbot-etc:/etc/letsencrypt 47 | networks: 48 | - app-network 49 | 50 | certbot: 51 | depends_on: 52 | - webserver 53 | image: certbot/certbot 54 | container_name: certbot 55 | volumes: 56 | - ./certbot-etc:/etc/letsencrypt 57 | - ./wordpress:/var/www/html 58 | command: certonly --webroot --webroot-path=/var/www/html --email --agree-tos --no-eff-email --staging -d example.com -d www.example.com 59 | 60 | phpmyadmin: 61 | image: phpmyadmin/phpmyadmin 62 | container_name: phpmyadmin 63 | ports: 64 | - "8081:80" 65 | environment: 66 | - PMA_HOST=db 67 | restart: always 68 | depends_on: 69 | - db 70 | networks: 71 | - app-network 72 | 73 | volumes: 74 | certbot-etc: 75 | wordpress: 76 | dbdata: 77 | nginx-conf: 78 | 79 | networks: 80 | app-network: 81 | driver: bridge 82 | -------------------------------------------------------------------------------- /10.dockercompose/demo1_wordpress/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: mysql:5.7 # mysql:latest 도 무방 6 | volumes: 7 | - mysql_vol:/var/lib/mysql # 볼륨 미사용시 ./mysql_data 8 | restart: always # unless-stopped 를 권장 9 | environment: 10 | MYSQL_ROOT_PASSWORD: this-is-my-root-password 11 | MYSQL_USER: wp-user # 사용자 계정 12 | MYSQL_PASSWORD: (******) # 사용자 암호 13 | MYSQL_DATABASE: wordpress # DB 이름 14 | networks: 15 | - app-network # 필요한 경우 16 | 17 | wordpress: 18 | depends_on: 19 | - db 20 | image: wordpress:latest 21 | ports: 22 | - "80:80" 23 | restart: always # unless-stopped 를 권장 24 | environment: 25 | WORDPRESS_DB_HOST: db:3306 # 기본포트라 그냥 db만 써주어도 무방 26 | WORDPRESS_DB_USER: wp-user # 사용자 계정 27 | WORDPRESS_DB_PASSWORD: (******) # 사용자 암호 28 | WORDPRESS_DB_NAME: wordpress # DB 이름 29 | volumes: 30 | - wordpress_vol:/var/www/html # 볼륨 미사용시 ./wordpress_data 31 | # - ./php/php.ini:/usr/local/etc/php/php.ini # 필요한 경우 32 | # - ./upload.ini:/usr/local/etc/php/conf.d/uploads.ini # 필요한 경우 33 | networks: 34 | - app-network # 필요한 경우 35 | 36 | volumes: 37 | mysql_vol: 38 | wordpress_vol: 39 | 40 | networks: 41 | app-network: 42 | driver: bridge 43 | -------------------------------------------------------------------------------- /10.dockercompose/demo2_myapp/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose up -d 2 | # docker-compose logs -f 3 | # curl localhost (80번 포트 통해 3개의 컨테이너로 서비스 되는 것 확인) 4 | version: '3' 5 | 6 | services: 7 | my-web: 8 | image: nginx:latest 9 | volumes: 10 | - ./nginx.conf:/etc/nginx/conf.d/default.conf 11 | ports: 12 | - 80:80 13 | depends_on: 14 | - my-flask1 15 | - my-flask2 16 | - my-flask3 17 | my-flask1: 18 | image: lovehyun/flask-app:1.3 19 | environment: 20 | - APP_COLOR=green 21 | my-flask2: 22 | image: lovehyun/flask-app:1.3 23 | environment: 24 | - APP_COLOR=orange 25 | my-flask3: 26 | image: lovehyun/flask-app:1.3 27 | environment: 28 | - APP_COLOR=red 29 | -------------------------------------------------------------------------------- /10.dockercompose/demo2_myapp/nginx.conf: -------------------------------------------------------------------------------- 1 | upstream my-apps { 2 | server my-flask1:5000; 3 | server my-flask2:5000; 4 | server my-flask3:5000; 5 | } 6 | 7 | server { 8 | listen 80; 9 | 10 | location / { 11 | proxy_pass http://my-apps/; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /10.dockercompose/docker-compose-eg1.yml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | 3 | services: 4 | app: 5 | image: lovehyun/flask-eg:1.0 6 | ports: 7 | - "8000:5000" 8 | -------------------------------------------------------------------------------- /10.dockercompose/docker-compose-eg2.yml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | 3 | services: 4 | app: 5 | image: lovehyun/flask-eg:1.0 6 | ports: 7 | - "8000:5000" 8 | 9 | database: 10 | image: mysql:5.7 11 | ports: 12 | - "3333:3306" 13 | environment: 14 | - MYSQL_ALLOW_EMPTY_PASSWORD=true 15 | volumes: 16 | - ./database:/var/lib/mysql 17 | -------------------------------------------------------------------------------- /10.dockercompose/docker-compose-eg3.yml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | 3 | services: 4 | app: 5 | image: lovehyun/flask-eg:1.0 6 | ports: 7 | - "8000:5000" 8 | links: 9 | - "database:db" 10 | networks: 11 | - my-app-network 12 | restart: unless-stopped 13 | depends_on: 14 | - database 15 | 16 | database: 17 | image: mysql:5.7 18 | ports: 19 | - "3333:3306" 20 | environment: 21 | - MYSQL_ALLOW_EMPTY_PASSWORD=true 22 | volumes: 23 | - my-database:/var/lib/mysql 24 | networks: 25 | - my-app-network 26 | restart: unless-stopped 27 | 28 | networks: 29 | my-app-network: 30 | 31 | volumes: 32 | my-database: 33 | -------------------------------------------------------------------------------- /10.dockercompose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | # 웹 애플리케이션 설정 6 | db: 7 | # 데이터베이스 설정 8 | 9 | volumes: 10 | # 볼륨 설정 11 | 12 | networks: 13 | # 네트워크 설정 14 | -------------------------------------------------------------------------------- /11.exercise/README.txt: -------------------------------------------------------------------------------- 1 | # a.reverse-proxy-local 2 | 3 | # standalone 서버에서 동작 테스트 4 | sudo systemctl start nginx 5 | sudo cp service1 /etc/nginx/sites-available/service 6 | sudo ln -s /etc/nginx/sites-available/service /etc/nginx/sites-enable/service 7 | sudo nginx -t 8 | sudo systemctl restart nginx 9 | 10 | # 단계별로 변경하며 테스트 11 | sudo cp service2 /etc/nginx/sites-available/service 12 | sudo cp service3 /etc/nginx/sites-available/service 13 | sudo cp service4 /etc/nginx/sites-available/service 14 | 15 | 16 | # proxy 서버 설정되어 올바른 client IP 가 로그에 주소가 출력되지 않을때 ProxyFix 사용 17 | # https://flask.palletsprojects.com/en/2.0.x/deploying/wsgi-standalone/ 18 | 19 | 20 | 21 | # b.reverse-proxy-docker 22 | 23 | # 도커 서비스 하나씩 띄워서 개별 확인 24 | docker run --name my-flask1 -d -p 8000:5000 -e APP_COLOR=green lovehyun/flask-app:1.2 25 | docker run --name my-flask2 -d -p 8001:5000 -e APP_COLOR=orange lovehyun/flask-app:1.2 26 | docker run --name my-flask3 -d -p 8002:5000 -e APP_COLOR=red lovehyun/flask-app:1.2 27 | 28 | curl localhost:8000 29 | curl localhost:8001 30 | curl localhost:8002 31 | 32 | 33 | # 웹 서비스 띄우고 reverse_proxy 확인 34 | 35 | # 설정 파일 내에서 IP 를 변경해 주어야 함 (아래 명령어로 IP 찾아서...) 36 | docker inspect -f "{{ .NetworkSettings.IPAddress }}" my-flask 37 | 38 | # 링크를 통해 컨테이너 이름으로 연결 할 수도 있음 39 | docker run -d --name my-nginx --link my-flask <옵션 중략> nginx 40 | 41 | # 설정파일 로컬에서 마운트 (full-path 를 사용해야 함) 42 | docker run --name my-nginx -v //nginx/conf:/etc/nginx/conf.d:ro -d -p 80:80 nginx 43 | docker run --name my-nginx -v //nginx/conf:/etc/nginx/conf.d:ro -d --link my-flask -d -p 80:80 nginx 44 | 45 | 46 | # 웹 서비스 설정 변경하고 reverse_proxy + load_balancing 확인 47 | docker run --name my-nginx -v //nginx/conf:/etc/nginx/conf.d:ro -d --link my-flask1:my-flask1 --link my-flask2:my-flask2 --link my-flask3:my-flask3 -d -p 80:80 nginx 48 | 49 | curl localhost 50 | 51 | 52 | 53 | # c.reverse-proxy-dockercompose 54 | 55 | # 모든 서비스 한번에 로딩 56 | docker-compose up -d 57 | docker-compose down 58 | -------------------------------------------------------------------------------- /11.exercise/a.reverse-proxy-local/service1: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | 4 | # root /var/www; 5 | # index index.html; 6 | 7 | location / { 8 | proxy_pass http://127.0.0.1:8000; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /11.exercise/a.reverse-proxy-local/service2: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | 4 | location /page1 { 5 | proxy_pass http://127.0.0.1:8000/; 6 | 7 | # proxy_set_header Host $http_host; 8 | # proxy_set_header X-Real-IP $remote_addr; 9 | # proxy_set_header X-Forwarded-For $remote_addr; 10 | # proxy_set_header X-Forwarded-Proto $scheme; 11 | 12 | include /etc/nginx/proxy_params; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /11.exercise/a.reverse-proxy-local/service3: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | 4 | location /page1 { 5 | # rewrite ^/page1(/.*)$ $1 break; 6 | proxy_pass http://127.0.0.1:8000/; 7 | 8 | include /etc/nginx/proxy_params; 9 | } 10 | 11 | location /page2 { 12 | proxy_pass http://127.0.0.1:8001/; 13 | 14 | include /etc/nginx/proxy_params; 15 | } 16 | 17 | location ^/page3(/|$) { 18 | proxy_pass http://127.0.0.1:8002/; 19 | 20 | include /etc/nginx/proxy_params; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /11.exercise/a.reverse-proxy-local/service4: -------------------------------------------------------------------------------- 1 | upstream my-apps { 2 | server 127.0.0.1:8000; 3 | server 127.0.0.1:8001; 4 | server 127.0.0.1:8002; 5 | } 6 | 7 | server { 8 | listen 8080; 9 | 10 | location / { 11 | proxy_pass http://my-apps; 12 | 13 | include /etc/nginx/proxy_params; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /11.exercise/b.reverse-proxy-docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | COPY nginx/conf/* /etc/nginx/conf.d/ 4 | 5 | CMD ["nginx", "-g", "daemon off;"] 6 | 7 | EXPOSE 80 8 | -------------------------------------------------------------------------------- /11.exercise/b.reverse-proxy-docker/nginx/conf/default.conf: -------------------------------------------------------------------------------- 1 | upstream my-apps1 { 2 | server my-flask1:5000; 3 | server my-flask2:5000; 4 | server my-flask3:5000; 5 | } 6 | 7 | upstream my-apps2 { 8 | server my-flask1:5000; 9 | server my-flask2:5000 backup; 10 | } 11 | 12 | server { 13 | listen 80; 14 | server_name localhost; 15 | 16 | location / { 17 | proxy_pass http://172.17.0.2:5000/; 18 | # proxy_pass http://my-flask1:5000/; 19 | # proxy_pass http://my-apps1; 20 | # proxy_pass http://my-apps2; 21 | 22 | # proxy_set_header Host $http_host; 23 | # proxy_set_header X-Real-IP $remote_addr; 24 | # proxy_set_header X-Forwarded-For $remote_addr; 25 | # proxy_set_header X-Forwarded-Proto $scheme; 26 | 27 | include conf.d/proxy_params; 28 | } 29 | 30 | error_page 404 /404.html; 31 | error_page 500 502 503 504 /50x.html; 32 | location = /50x.html { 33 | root /usr/share/nginx/html; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /11.exercise/b.reverse-proxy-docker/nginx/conf/proxy_params: -------------------------------------------------------------------------------- 1 | proxy_set_header Host $http_host; 2 | proxy_set_header X-Real-IP $remote_addr; 3 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 4 | proxy_set_header X-Forwarded-Proto $scheme; 5 | -------------------------------------------------------------------------------- /11.exercise/c.reverse-proxy-dockercompose/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | 3 | services: 4 | my-app1: 5 | image: lovehyun/flask-app:1.2 6 | environment: 7 | - APP_COLOR=green 8 | ports: 9 | - "8000:5000" 10 | networks: 11 | - my-app-network 12 | restart: unless-stopped 13 | 14 | my-app2: 15 | image: lovehyun/flask-app:1.2 16 | environment: 17 | - APP_COLOR=orange 18 | ports: 19 | - "8001:5000" 20 | networks: 21 | - my-app-network 22 | restart: unless-stopped 23 | 24 | my-app3: 25 | image: lovehyun/flask-app:1.2 26 | environment: 27 | - APP_COLOR=red 28 | ports: 29 | - "8002:5000" 30 | networks: 31 | - my-app-network 32 | restart: unless-stopped 33 | 34 | my-nginx: 35 | image: lovehyun/my-nginx:1.0 36 | # image: nginx:latest 37 | ports: 38 | - "80:80" 39 | links: 40 | - "my-app1:my-flask1" 41 | - "my-app2:my-flask2" 42 | - "my-app3:my-flask3" 43 | # volumes: 44 | # - ./nginx/conf:/etc/nginx/conf.d 45 | networks: 46 | - my-app-network 47 | restart: unless-stopped 48 | depends_on: 49 | - my-app1 50 | - my-app2 51 | - my-app3 52 | 53 | networks: 54 | my-app-network: 55 | -------------------------------------------------------------------------------- /11.exercise/c.reverse-proxy-dockercompose/nginx/conf/default.conf: -------------------------------------------------------------------------------- 1 | upstream my-apps1 { 2 | server my-flask1:8000; 3 | server my-flask2:8001; 4 | server my-flask3:8002; 5 | # server my-flask1:5000; 6 | # server my-flask2:5000; 7 | # server my-flask3:5000; 8 | } 9 | 10 | upstream my-apps2 { 11 | server my-flask1:5000; 12 | server my-flask2:5000 backup; 13 | } 14 | 15 | server { 16 | listen 80; 17 | server_name localhost; 18 | 19 | location / { 20 | #proxy_pass http://172.17.0.2:5000/; 21 | # proxy_pass http://my-flask1:5000/; 22 | proxy_pass http://my-apps1; 23 | # proxy_pass http://my-apps2; 24 | 25 | # proxy_set_header Host $http_host; 26 | # proxy_set_header X-Real-IP $remote_addr; 27 | # proxy_set_header X-Forwarded-For $remote_addr; 28 | # proxy_set_header X-Forwarded-Proto $scheme; 29 | 30 | include conf.d/proxy_params; 31 | } 32 | 33 | error_page 404 /404.html; 34 | error_page 500 502 503 504 /50x.html; 35 | location = /50x.html { 36 | root /usr/share/nginx/html; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /11.exercise/c.reverse-proxy-dockercompose/nginx/conf/proxy_params: -------------------------------------------------------------------------------- 1 | proxy_set_header Host $http_host; 2 | proxy_set_header X-Real-IP $remote_addr; 3 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 4 | proxy_set_header X-Forwarded-Proto $scheme; 5 | -------------------------------------------------------------------------------- /12.buildx/README.md: -------------------------------------------------------------------------------- 1 | # Docker Build Deprecated Warning and Buildx Migration 2 | 3 | `docker build` 명령어를 사용할 때 `deprecated` 경고 메시지와 함께 `docker buildx`를 사용하라는 메시지가 나타나는 이유는, Docker가 기존의 `docker build`를 더 이상 권장하지 않고, 멀티 플랫폼 이미지를 지원하고 더 향상된 빌드 기능을 제공하는 `docker buildx`를 기본으로 채택했기 때문입니다. 4 | 5 | ## `docker buildx`란? 6 | - **멀티 플랫폼 빌드 지원:** `docker buildx`는 ARM, x86 등 다양한 아키텍처용 이미지를 동시에 빌드할 수 있습니다. 7 | - **캐싱 개선:** 고급 빌드 캐시를 제공하여 빌드 속도를 개선합니다. 8 | - **분산 빌드:** 여러 노드를 사용한 분산 빌드 지원. 9 | - **Dockerfile 호환:** 기존 Dockerfile과 호환됩니다. 10 | 11 | ## `docker build`에서 `docker buildx`로 전환 방법 12 | 13 | 1. **기본적으로 `buildx` 활성화하기** 14 | ```bash 15 | docker buildx install 16 | ``` 17 | 18 | 2. **기본 빌더 확인 및 설정** 19 | ```bash 20 | docker buildx create --use 21 | ``` 22 | 23 | ### `docker buildx` 설치 및 설정 24 | 25 | `docker buildx`가 기본적으로 포함되지 않았을 경우, 아래와 같이 수동으로 설치할 수 있습니다: 26 | 27 | ```bash 28 | mkdir -p ~/.docker/cli-plugins/ 29 | curl -SL https://github.com/docker/buildx/releases/download/v0.15.0/buildx-v0.15.0.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx 30 | chmod +x ~/.docker/cli-plugins/docker-buildx 31 | ``` 32 | 33 | ### `docker buildx` 버전 확인 34 | ```bash 35 | docker buildx version 36 | github.com/docker/buildx v0.15.0 d3a53189f7e9c917eeff851c895b9aad5a66b108 37 | ``` 38 | 39 | ### `docker buildx create --use`의 기능과 역할 40 | - **빌더 인스턴스 생성:** `docker buildx create`는 새로운 빌더 인스턴스를 생성합니다. 빌더 인스턴스는 도커 이미지 빌드를 실행할 수 있는 환경을 의미합니다. 41 | - **멀티플랫폼 지원:** 생성된 빌더는 멀티플랫폼 빌드를 지원할 수 있는 컨텍스트를 제공합니다. 42 | - **빌더 활성화 (`--use` 옵션):** `--use` 옵션은 새로 생성한 빌더를 기본 빌더로 설정하여 이후의 `docker buildx build` 명령어에 적용되도록 합니다. 43 | 44 | ### `docker buildx create`로 생성된 빌더 확인 방법 45 | - **생성된 빌더 리스트 확인:** 46 | ```bash 47 | docker buildx ls 48 | ``` 49 | - **빌더 상세 정보:** 50 | ```bash 51 | docker buildx inspect --bootstrap 52 | ``` 53 | - **빌더 인스턴스 삭제:** 54 | ```bash 55 | docker buildx rm mybuilder 56 | ``` 57 | - **로컬에 저장 경로:** 58 | 생성된 빌더는 Docker 컨텍스트 내에 저장되며, Docker Desktop에서는 기본적으로 `~/.docker` 경로에 해당 정보가 저장됩니다. 59 | 60 | ### `docker buildx create --use` 사용 예제 61 | ```bash 62 | docker buildx create --name mybuilder --use 63 | docker buildx build --platform linux/amd64,linux/arm64 -t myimage:latest . 64 | ``` 65 | 66 | 3. **이미지 빌드 명령어 예제 (기존과 동일하게 사용 가능)** 67 | ```bash 68 | docker buildx build -t myimage:latest . 69 | ``` 70 | 71 | 4. **이미지 로컬 Docker 데몬에 로드하기** 72 | ```bash 73 | docker buildx build . -t hello --load 74 | ``` 75 | 76 | 5. **이미지 레지스트리에 푸시하기** 77 | ```bash 78 | docker buildx build . -t username/hello --push 79 | ``` 80 | 81 | 6. **멀티 아키텍처 빌드 예제** 82 | ```bash 83 | docker buildx build --platform linux/amd64,linux/arm64 -t hello --load . 84 | ``` 85 | 86 | ## ⚠️ 주의사항 87 | - `docker buildx`는 Docker Desktop 19.03 이상 버전에서 기본 제공되며, `buildx`가 없는 경우 수동 설치가 필요할 수 있습니다. 88 | - `docker buildx`는 기존 `docker build`보다 더 강력하지만, 일부 오래된 기능은 제거되었을 수 있습니다. 89 | 90 | 이제 `docker build` 대신 `docker buildx`를 사용하는 것이 권장됩니다. 91 | -------------------------------------------------------------------------------- /2.nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | MAINTAINER shpark 4 | 5 | RUN apt-get update && \ 6 | apt-get install nginx -y && \ 7 | echo "\ndaemon off;" >> /etc/nginx/nginx.conf && \ 8 | sed -i 's|access_log /var/log/nginx/access.log;|access_log /dev/stdout;|g' /etc/nginx/nginx.conf && \ 9 | sed -i 's|error_log /var/log/nginx/error.log;|error_log /dev/stderr;|g' /etc/nginx/nginx.conf 10 | 11 | RUN echo "

Hello SOMA

" > /var/www/html/index.html 12 | 13 | # Clean up APT when done. 14 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 15 | 16 | ENTRYPOINT ["nginx"] 17 | # CMD ["nginx"] 18 | # CMD ["nginx", "-g", "daemon off;"] 19 | # CMD ["nginx", "-g", "daemon off;", "-c", "/etc/nginx/nginx.conf", "-g", "error_log /dev/stderr; access_log /dev/stdout;"] 20 | 21 | EXPOSE 80 22 | -------------------------------------------------------------------------------- /2.nginx/README.md: -------------------------------------------------------------------------------- 1 | # Dockerfile 내의 주요 옵션 2 | ## 우분투(ubuntu) 기반의 이미지에 타임존 설정 3 | - ARG 는 docker build 에서만 설정되는 환경변수 4 | - ENV 는 시스템에 설정되는 환경변수 5 | 6 | ``` 7 | ARG DEBIAN_FRONTEND=noninteractive 8 | ENV TZ=Asia/Seoul 9 | RUN apt-get install -y tzdata 10 | ``` 11 | 12 | ## 알파인(alpine) 기반의 이미지에 타임존 설정 13 | ``` 14 | RUN apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime 15 | ``` 16 | 17 | ## 우분투에서 dpkg 관련 warning 해결 18 | ``` 19 | RUN apt-get update && apt-get upgrade -y -o Dpkg::Options::="--force-confold" 20 | ``` 21 | 22 | ## Clean up APT when done. 23 | ``` 24 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 25 | ``` 26 | 27 | 28 | # 도커 유용한 명령어 29 | 30 | ## Dangling 이미지 삭제하기 31 | ``` 32 | docker rmi $(docker images -f "dangling=true" -q) 33 | ``` 34 | 또는 최신 명령어 35 | ``` 36 | docker image prune 37 | ``` 38 | 39 | 40 | # Alpine 리눅스 주요 명령어 41 | 42 | ## 알파인(alpine) 리눅스의 기본 패키지 업데이트 및 설정 43 | ``` 44 | apk update 45 | apk add curl 46 | ``` 47 | -------------------------------------------------------------------------------- /20.scratch/README.txt: -------------------------------------------------------------------------------- 1 | # Setup 2 | https://wiki.archlinux.org/title/Sudo 3 | alias sudo='sudo ' 4 | alias my-docker="go run main.go" 5 | cp main2.go main.go 6 | 7 | 8 | # main1.go 9 | Basic template - 실제 동작 안함 10 | 11 | 12 | 13 | # main2.go 14 | 프로세스 포크해서 hello world, ls 등 실행하는 기본 golang 코드 15 | 16 | go run main2.go run echo hello world 17 | go run main2.go run ls 18 | go run main2.go run ls -l 19 | 20 | 21 | 22 | # main3.go 23 | UTS (Unix Time-sharing System = hostname) 격리 예제 24 | 25 | sudo go run main3.go run hostname 26 | sudo go run main3.go run hostname container 27 | 28 | sudo go run main3.go run bash 29 | hostname container 30 | 31 | 32 | 33 | # main4.go 34 | 실행 시 바로 격리된 호스트네임으로 출력 예제 35 | 36 | sudo go run main4.go run bash 37 | 호스트네임 쉘프롬프트에 PS1 38 | 39 | ps 40 | ps x 41 | <프로세스는 아직 미격리> 42 | 43 | 44 | 45 | # main5.go 46 | 프로세스 분리 시도 (그러나 프로세스가 분리 되더라도 proc 파일시스템으로 인해 동작 X) 47 | 48 | sudo go run main5.go run bash 49 | <시작시 PID> 50 | 51 | ps 52 | ps x 53 | 54 | 55 | 56 | 57 | # main6.go 58 | 여러개의 도커가 이것저것 파일 시스템 마운트 하게 된다면? (NS = Filesystem 격리) 59 | 60 | sudo go run main6.go run bash 61 | <디렉토리 격리> 62 | ps 63 | ps x 64 | <격리> 65 | mount 66 | 67 | 68 | ps -C sleep 69 | ls -l /proc//root 70 | cat /proc//mounts 71 | 72 | 73 | 74 | # main7.go 75 | 그래서 ROOT_DIR 새로 생성해서 작업 (편의 상 ubuntu 이미지를 기반으로 새로운 루트파일 시스템 생성) 76 | 77 | mkdir ROOT_DIR 78 | cd ROOT_DIR 79 | touch CONTAINER_ROOT 80 | mkdir bin 81 | mkdir etc 82 | mkdir lib 83 | mkdir lib64 84 | mkdir proc 85 | cp -r /bin . 86 | cp -r /lib/x86_64-linux-gnu ./lib 87 | cp -r /lib64 . 88 | 또는... 89 | docker export $(docker create ubuntu:20.04) | tar -xf - -C ROOT_DIR 90 | 91 | 92 | sudo go run main7.go run bash 93 | <디렉토리 격리> 94 | 95 | ps 96 | <안됨> 97 | 98 | mount -t proc proc /proc 99 | 100 | ln -s /proc/self/mounts /etc/mtab 101 | mount 102 | 103 | 104 | 105 | 106 | # main8.go 107 | 프로세스 격리, ROOT 파이 시스템 마운트, proc 파일시스템 자동 마운트 예제 108 | 109 | sudo go run main8.go run bash 110 | <디렉토리 격리> 111 | 112 | ps 113 | ps x 114 | <격리> 115 | 116 | 117 | 118 | # main9.go 119 | 120 | cd /sys/fs/cgroup/memory 121 | cat memory.limit_in_bytes 122 | docker run --rm -it ubuntu /bin/bash 123 | docker run --rm -it ubuntu --memory=10M /bin/bash 124 | cat docker/xxxxx/memory.limit_in_bytes 125 | 126 | 127 | -------------------------------------------------------------------------------- /20.scratch/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | "path/filepath" 9 | "io/ioutil" 10 | "strconv" 11 | ) 12 | 13 | func main() { 14 | switch os.Args[1] { 15 | case "run": 16 | run() 17 | case "child": 18 | child() 19 | default: 20 | panic("help") 21 | } 22 | } 23 | 24 | func run() { 25 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 26 | 27 | cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...) 28 | cmd.Stdin = os.Stdin 29 | cmd.Stdout = os.Stdout 30 | cmd.Stderr = os.Stderr 31 | cmd.SysProcAttr = &syscall.SysProcAttr { 32 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, 33 | // Unshareflags: syscall.CLONE_NEWNS, 34 | } 35 | 36 | must(cmd.Run()) 37 | } 38 | 39 | func child() { 40 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 41 | 42 | cg() 43 | 44 | cmd := exec.Command(os.Args[2], os.Args[3:]...) 45 | cmd.Stdin = os.Stdin 46 | cmd.Stdout = os.Stdout 47 | cmd.Stderr = os.Stderr 48 | 49 | must(syscall.Sethostname([]byte("container"))) 50 | 51 | syscall.Chroot("./ROOT_DIR") 52 | syscall.Chdir("/") 53 | 54 | syscall.Mount("proc", "proc", "proc", 0, "") 55 | 56 | must(cmd.Run()) 57 | 58 | syscall.Unmount("proc", 0) 59 | } 60 | 61 | func must(err error) { 62 | if err != nil { 63 | panic(err) 64 | } 65 | } 66 | 67 | func cg() { 68 | cgroups := "/sys/fs/cgroup/" 69 | pids := filepath.Join(cgroups, "pids") 70 | os.Mkdir(filepath.Join(pids, "hyun"), 0755) 71 | must(ioutil.WriteFile(filepath.Join(pids, "hyun/pids.max"), []byte("10"), 0700)) 72 | // Remove the new cgroup in place after the container exits 73 | must(ioutil.WriteFile(filepath.Join(pids, "hyun/notify_on_release"), []byte("1"), 0700)) 74 | must(ioutil.WriteFile(filepath.Join(pids, "hyun/cgroup.procs"), []byte(strconv.Itoa(os.Getpid())), 0700)) 75 | } 76 | -------------------------------------------------------------------------------- /20.scratch/main1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | // docker run cmd args 10 | // go run main.go run cmd args 11 | func main() { 12 | 13 | } 14 | 15 | func run() { 16 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 17 | 18 | cmd := exec.Command() 19 | cmd.Stdin = os.Stdin 20 | cmd.Stdout = os.Stdout 21 | cmd.Stderr = os.Stderr 22 | 23 | must(cmd.Run()) 24 | } 25 | 26 | func must(err error) { 27 | if err != nil { 28 | panic(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /20.scratch/main2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | // go run main.go run echo hello world 10 | // go run main.go run ls -l 11 | func main() { 12 | switch os.Args[1] { 13 | case "run": 14 | run() 15 | default: 16 | panic("help") 17 | } 18 | } 19 | 20 | func run() { 21 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 22 | 23 | cmd := exec.Command(os.Args[2], os.Args[3:]...) 24 | cmd.Stdin = os.Stdin 25 | cmd.Stdout = os.Stdout 26 | cmd.Stderr = os.Stderr 27 | 28 | must(cmd.Run()) 29 | } 30 | 31 | func must(err error) { 32 | if err != nil { 33 | panic(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /20.scratch/main3.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | // go run main.go run hostname 11 | // go run main.go run bash -> hostname container 12 | func main() { 13 | switch os.Args[1] { 14 | case "run": 15 | run() 16 | default: 17 | panic("help") 18 | } 19 | } 20 | 21 | func run() { 22 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 23 | 24 | cmd := exec.Command(os.Args[2], os.Args[3:]...) 25 | cmd.Stdin = os.Stdin 26 | cmd.Stdout = os.Stdout 27 | cmd.Stderr = os.Stderr 28 | cmd.SysProcAttr = &syscall.SysProcAttr { 29 | Cloneflags: syscall.CLONE_NEWUTS, 30 | } 31 | 32 | // syscall.Sethostname([]byte("container")) 33 | must(cmd.Run()) 34 | } 35 | 36 | func must(err error) { 37 | if err != nil { 38 | panic(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /20.scratch/main4.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | func main() { 11 | switch os.Args[1] { 12 | case "run": 13 | run() 14 | case "child": 15 | child() 16 | default: 17 | panic("help") 18 | } 19 | } 20 | 21 | func run() { 22 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 23 | 24 | cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...) 25 | cmd.Stdin = os.Stdin 26 | cmd.Stdout = os.Stdout 27 | cmd.Stderr = os.Stderr 28 | cmd.SysProcAttr = &syscall.SysProcAttr { 29 | Cloneflags: syscall.CLONE_NEWUTS, 30 | } 31 | 32 | must(cmd.Run()) 33 | } 34 | 35 | func child() { 36 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 37 | 38 | cmd := exec.Command(os.Args[2], os.Args[3:]...) 39 | cmd.Stdin = os.Stdin 40 | cmd.Stdout = os.Stdout 41 | cmd.Stderr = os.Stderr 42 | 43 | must(syscall.Sethostname([]byte("container"))) 44 | 45 | must(cmd.Run()) 46 | } 47 | 48 | func must(err error) { 49 | if err != nil { 50 | panic(err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /20.scratch/main5.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | func main() { 11 | switch os.Args[1] { 12 | case "run": 13 | run() 14 | case "child": 15 | child() 16 | default: 17 | panic("help") 18 | } 19 | } 20 | 21 | func run() { 22 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 23 | 24 | cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...) 25 | cmd.Stdin = os.Stdin 26 | cmd.Stdout = os.Stdout 27 | cmd.Stderr = os.Stderr 28 | cmd.SysProcAttr = &syscall.SysProcAttr { 29 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID, 30 | } 31 | 32 | must(cmd.Run()) 33 | } 34 | 35 | func child() { 36 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 37 | 38 | cmd := exec.Command(os.Args[2], os.Args[3:]...) 39 | cmd.Stdin = os.Stdin 40 | cmd.Stdout = os.Stdout 41 | cmd.Stderr = os.Stderr 42 | 43 | must(syscall.Sethostname([]byte("container"))) 44 | 45 | must(cmd.Run()) 46 | } 47 | 48 | func must(err error) { 49 | if err != nil { 50 | panic(err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /20.scratch/main6.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | func main() { 11 | switch os.Args[1] { 12 | case "run": 13 | run() 14 | case "child": 15 | child() 16 | default: 17 | panic("help") 18 | } 19 | } 20 | 21 | func run() { 22 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 23 | 24 | cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...) 25 | cmd.Stdin = os.Stdin 26 | cmd.Stdout = os.Stdout 27 | cmd.Stderr = os.Stderr 28 | cmd.SysProcAttr = &syscall.SysProcAttr { 29 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, 30 | // Unshareflags: syscall.CLONE_NEWNS, 31 | } 32 | 33 | must(cmd.Run()) 34 | } 35 | 36 | func child() { 37 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 38 | 39 | cmd := exec.Command(os.Args[2], os.Args[3:]...) 40 | cmd.Stdin = os.Stdin 41 | cmd.Stdout = os.Stdout 42 | cmd.Stderr = os.Stderr 43 | 44 | must(syscall.Sethostname([]byte("container"))) 45 | 46 | syscall.Chroot("./ROOT_DIR") 47 | syscall.Chdir("/") 48 | 49 | syscall.Mount("proc", "proc", "proc", 0, "") 50 | 51 | must(cmd.Run()) 52 | 53 | syscall.Unmount("proc", 0) 54 | } 55 | 56 | func must(err error) { 57 | if err != nil { 58 | panic(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /20.scratch/main7.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | func main() { 11 | switch os.Args[1] { 12 | case "run": 13 | run() 14 | case "child": 15 | child() 16 | default: 17 | panic("help") 18 | } 19 | } 20 | 21 | func run() { 22 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 23 | 24 | cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...) 25 | cmd.Stdin = os.Stdin 26 | cmd.Stdout = os.Stdout 27 | cmd.Stderr = os.Stderr 28 | cmd.SysProcAttr = &syscall.SysProcAttr { 29 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID, 30 | } 31 | 32 | must(cmd.Run()) 33 | } 34 | 35 | func child() { 36 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 37 | 38 | cmd := exec.Command(os.Args[2], os.Args[3:]...) 39 | cmd.Stdin = os.Stdin 40 | cmd.Stdout = os.Stdout 41 | cmd.Stderr = os.Stderr 42 | 43 | must(syscall.Sethostname([]byte("container"))) 44 | 45 | syscall.Chroot("./ROOT_DIR") 46 | syscall.Chdir("/") 47 | 48 | must(cmd.Run()) 49 | } 50 | 51 | func must(err error) { 52 | if err != nil { 53 | panic(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /20.scratch/main8.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | func main() { 11 | switch os.Args[1] { 12 | case "run": 13 | run() 14 | case "child": 15 | child() 16 | default: 17 | panic("help") 18 | } 19 | } 20 | 21 | func run() { 22 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 23 | 24 | cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...) 25 | cmd.Stdin = os.Stdin 26 | cmd.Stdout = os.Stdout 27 | cmd.Stderr = os.Stderr 28 | cmd.SysProcAttr = &syscall.SysProcAttr { 29 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID, 30 | } 31 | 32 | must(cmd.Run()) 33 | } 34 | 35 | func child() { 36 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 37 | 38 | cmd := exec.Command(os.Args[2], os.Args[3:]...) 39 | cmd.Stdin = os.Stdin 40 | cmd.Stdout = os.Stdout 41 | cmd.Stderr = os.Stderr 42 | 43 | must(syscall.Sethostname([]byte("container"))) 44 | 45 | syscall.Chroot("./ROOT_DIR") 46 | syscall.Chdir("/") 47 | 48 | syscall.Mount("proc", "proc", "proc", 0, "") 49 | 50 | must(cmd.Run()) 51 | 52 | syscall.Unmount("proc", 0) 53 | } 54 | 55 | func must(err error) { 56 | if err != nil { 57 | panic(err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /20.scratch/main9.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "syscall" 8 | "path/filepath" 9 | "io/ioutil" 10 | "strconv" 11 | ) 12 | 13 | func main() { 14 | switch os.Args[1] { 15 | case "run": 16 | run() 17 | case "child": 18 | child() 19 | default: 20 | panic("help") 21 | } 22 | } 23 | 24 | func run() { 25 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 26 | 27 | cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...) 28 | cmd.Stdin = os.Stdin 29 | cmd.Stdout = os.Stdout 30 | cmd.Stderr = os.Stderr 31 | cmd.SysProcAttr = &syscall.SysProcAttr { 32 | Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, 33 | // Unshareflags: syscall.CLONE_NEWNS, 34 | } 35 | 36 | must(cmd.Run()) 37 | } 38 | 39 | func child() { 40 | fmt.Printf("Running %v as pid %d\n", os.Args[2:], os.Getpid()) 41 | 42 | cg() 43 | 44 | cmd := exec.Command(os.Args[2], os.Args[3:]...) 45 | cmd.Stdin = os.Stdin 46 | cmd.Stdout = os.Stdout 47 | cmd.Stderr = os.Stderr 48 | 49 | must(syscall.Sethostname([]byte("container"))) 50 | 51 | syscall.Chroot("./ROOT_DIR") 52 | syscall.Chdir("/") 53 | 54 | syscall.Mount("proc", "proc", "proc", 0, "") 55 | 56 | must(cmd.Run()) 57 | 58 | syscall.Unmount("proc", 0) 59 | } 60 | 61 | func must(err error) { 62 | if err != nil { 63 | panic(err) 64 | } 65 | } 66 | 67 | func cg() { 68 | cgroups := "/sys/fs/cgroup/" 69 | pids := filepath.Join(cgroups, "pids") 70 | os.Mkdir(filepath.Join(pids, "hyun"), 0755) 71 | must(ioutil.WriteFile(filepath.Join(pids, "hyun/pids.max"), []byte("10"), 0700)) 72 | // Remove the new cgroup in place after the container exits 73 | must(ioutil.WriteFile(filepath.Join(pids, "hyun/notify_on_release"), []byte("1"), 0700)) 74 | must(ioutil.WriteFile(filepath.Join(pids, "hyun/cgroup.procs"), []byte(strconv.Itoa(os.Getpid())), 0700)) 75 | } 76 | -------------------------------------------------------------------------------- /21.api/README.md: -------------------------------------------------------------------------------- 1 | # 도커의 환경 구성 2 | /lib/systemd/system/docker.service 파일을 통한 각종 옵션 정의 3 | 4 | ## 추가적인 TCP 소켓 생성 5 | 6 | ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock 7 | 8 | ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H 0.0.0.0:12345 9 | 10 | 로 변경하게 되면 도커 서비스 재시작 이후 curl localhost:12345 통해 REST 통신 가능 11 | 12 | 13 | # Unix domain socket 을 통한 REST 인터페이스 연동 14 | 15 | 위 설정을 통해 TCP 통신을 하지 않더라도, unix domain 소켓을 통해 기본적으로 REST 통신이 가능 함. 16 | 17 | curl --unix-socket /var/run/docker.sock http://localhost/images/json | jq 18 | 19 | curl --unix-socket /var/run/docker.sock http://localhost/containers/json | jq 20 | 21 | 22 | 그 외에도 컨테이너의 상태나 설정 등도 가능 함. 23 | 24 | curl --unix-socket http://localhost/containers/xxxx/json | jq -r '.State.Status' 25 | 26 | curl --unix-socket -X POST http://localhost/containers/xxxx/stop | jq -r '.' 27 | 28 | -------------------------------------------------------------------------------- /22.security/README.md: -------------------------------------------------------------------------------- 1 | # 도커 보안 고려사항 2 | 3 | ## 리소스 할당량 활용 4 | 도커 컨테이너의 리소스를 제한하여 자원 과다 사용을 방지할 수 있습니다. 5 | ```bash 6 | docker run --memory --cpus 7 | ``` 8 | 9 | ## 도커 컨테이너 루트로 실행 방지 10 | 컨테이너를 루트로 실행하는 것은 보안상 위험할 수 있습니다. 비루트 사용자로 실행하거나, 이미지를 생성할 때 계정을 생성하여 보안을 강화할 수 있습니다. 11 | ```bash 12 | docker run -u 13 | ``` 14 | ### 이미지 내 사용자 계정 생성 15 | ```dockerfile 16 | FROM alpine 17 | RUN groupadd -r mygroup && useradd -r -g mygroup user 18 | USER user 19 | ``` 20 | 21 | ### 기본 사용자 계정 설정 (데몬 설정) 22 | ```json 23 | { 24 | "userns-remap": "default" 25 | } 26 | ``` 27 | 28 | ## 도커 컨테이너 레지스트리의 보안 유지 29 | - 신뢰할 수 있는 공식 레지스트리 사용 30 | - 프라이빗 레지스트리 사용 시 인증 및 접근 제어 적용 31 | - 이미지 서명 및 검증 적용 (Docker Content Trust 사용) 32 | ```bash 33 | export DOCKER_CONTENT_TRUST=1 34 | ``` 35 | 36 | ## 보안을 염두해 둔 API 및 네트워크 설계 37 | - Docker API는 TLS를 사용하여 보호 38 | - 불필요한 포트 노출 방지 39 | - 네트워크 세그먼트 분리 및 방화벽 사용 40 | 41 | ## 컨테이너 내 라이브러리/소스코드 분석 42 | 이미지 내 취약점을 점검하기 위해 오픈소스 도구를 활용할 수 있습니다. 43 | ```bash 44 | trivy image 45 | clairctl analyze 46 | ``` 47 | 48 | --- 49 | 50 | # 도커 보안 체크리스트 51 | 52 | ## 1. 최신 버전 업데이트 53 | - 도커 엔진, 플러그인 및 관련 도구를 최신 버전으로 유지 54 | 55 | ## 2. 도커 데몬 소켓 관리 56 | - TCP 소켓 오픈 시 인증 및 암호화 적용 57 | - `/var/run/docker.sock` 노출 금지 58 | 59 | ## 3. 루트 실행 방지 60 | - 사용자 계정 생성 및 비루트 계정 실행 적용 61 | 62 | ## 4. 커널 레벨 역할/권한 제한 (capabilities 관리) 63 | - 불필요한 커널 권한 제거 64 | ```bash 65 | docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE 66 | ``` 67 | 68 | ## 5. CVE 체크 도구 활용 69 | - `clair`, `trivy` 등의 도구를 이용한 이미지 취약점 검사 70 | 71 | ## 6. 도커 파일 보안 72 | - `FROM` 구문에 신뢰할 수 있는 공식 이미지 사용 73 | - `COPY` 대신 `ADD` 사용 지양 (URL 다운로드 기능 때문) 74 | 75 | ## 7. 이미지 서명 및 검증 76 | - Docker Content Trust 활성화 77 | 78 | ## 8. 네트워크 보안 79 | - 브릿지 네트워크 대신 사용자 정의 네트워크 사용 80 | - 네트워크 트래픽 제어 및 방화벽 설정 적용 81 | 82 | ## 9. 데이터 보호 83 | - 데이터 암호화 및 보관 정책 적용 84 | 85 | ## 10. 로깅 및 모니터링 86 | - 도커 로그 드라이버 구성 87 | - 보안 이벤트 모니터링 및 알림 설정 88 | -------------------------------------------------------------------------------- /22.security/express_docker_security.md: -------------------------------------------------------------------------------- 1 | # Express 기반 Docker 보안 가이드 2 | 3 | ## 📦 1. 최소 권한 원칙 (Least Privilege) 4 | - **루트 사용자 금지:** 애플리케이션을 실행할 사용자 계정을 생성하고, 해당 계정으로 컨테이너를 실행합니다. 5 | ```dockerfile 6 | # 베이스 이미지로 공식 Node.js 버전 사용 (Alpine은 경량화 버전) 7 | FROM node:18-alpine 8 | 9 | # 애플리케이션 경로 및 사용자 생성 10 | WORKDIR /app 11 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup 12 | 13 | # 사용자 권한 변경 14 | USER appuser 15 | ``` 16 | 17 | ## 🔒 2. 종속성 검증 및 최신 버전 유지 18 | - **의존성 검증:** `package.json` 내 불필요하거나 취약한 패키지 제거 19 | - **의존성 최신화 및 취약점 점검:** 20 | ```bash 21 | npm audit fix 22 | npm update 23 | ``` 24 | 25 | ## 📑 3. 다중 스테이지 빌드 (Multi-Stage Build) 26 | - **빌드와 실행을 분리:** 불필요한 빌드 도구 제거 27 | ```dockerfile 28 | # 빌드 스테이지 29 | FROM node:18-alpine AS builder 30 | WORKDIR /app 31 | COPY package*.json ./ 32 | RUN npm ci --only=production 33 | 34 | # 실행 스테이지 35 | FROM node:18-alpine 36 | WORKDIR /app 37 | COPY --from=builder /app . 38 | USER appuser 39 | CMD ["node", "index.js"] 40 | ``` 41 | 42 | ## 🔥 4. 불필요한 권한 제거 (Capabilities 및 Privileges) 43 | - **권한 제한:** `--cap-drop` 및 `--cap-add` 활용 44 | ```bash 45 | docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE 46 | ``` 47 | 48 | ## 📶 5. 네트워크 보안 및 방화벽 49 | - **네트워크 세분화:** 브릿지 네트워크 대신 사용자 정의 네트워크 사용 50 | ```bash 51 | docker network create --driver bridge secure_network 52 | docker run --network secure_network 53 | ``` 54 | 55 | ## 📜 6. 환경변수 보안 및 `.env` 관리 56 | - **민감한 정보 관리:** `.env` 파일을 `.dockerignore`에 추가 57 | ```dockerignore 58 | .env 59 | node_modules 60 | ``` 61 | - **환경변수 주입:** `Dockerfile`에 하드코딩 금지 62 | ```bash 63 | docker run --env-file .env 64 | ``` 65 | 66 | ## 🔗 7. 이미지 서명 및 검증 67 | - **신뢰할 수 있는 이미지 사용:** 서명 활성화 68 | ```bash 69 | export DOCKER_CONTENT_TRUST=1 70 | ``` 71 | 72 | ## 🛡️ 8. CVE 점검 및 보안 스캐닝 73 | - **취약점 점검:** `trivy` 또는 `clair` 사용 74 | ```bash 75 | trivy image 76 | clairctl analyze 77 | ``` 78 | 79 | ## 📂 9. 파일 시스템 및 경로 보호 80 | - **읽기 전용 파일 시스템 적용:** 81 | ```bash 82 | docker run --read-only 83 | ``` 84 | - **특정 볼륨만 쓰기 가능:** 85 | ```bash 86 | docker run --read-only -v /data/app:rw 87 | ``` 88 | 89 | ## ✅ 보안 강화 요약 체크리스트 90 | - [x] 루트 사용자 실행 방지 (`USER` 명령어 사용) 91 | - [x] 최소한의 권한만 부여 (`cap-drop=ALL`) 92 | - [x] 다중 스테이지 빌드 활용 93 | - [x] 불필요한 소프트웨어 제거 (경량화 이미지 사용) 94 | - [x] 환경변수 보안 (`.env` 파일 관리) 95 | - [x] 취약점 스캔 (`trivy`, `clair`) 96 | - [x] 서명된 이미지 사용 (`DOCKER_CONTENT_TRUST`) 97 | - [x] 파일 시스템 읽기 전용 모드 (`--read-only`) 98 | -------------------------------------------------------------------------------- /22.security/flask_docker_security.md: -------------------------------------------------------------------------------- 1 | # Flask 기반 Docker 보안 가이드 2 | 3 | ## 📦 1. 최소 권한 원칙 (Least Privilege) 4 | - **루트 사용자 금지:** 비루트 계정을 생성하고, 해당 계정으로 Flask 앱을 실행합니다. 5 | ```dockerfile 6 | # 경량 Python 이미지 사용 및 비루트 사용자 설정 7 | FROM python:3.11-slim 8 | 9 | # 사용자 생성 및 설정 10 | RUN groupadd -r appgroup && useradd -r -g appgroup appuser 11 | 12 | # 작업 디렉토리 설정 및 권한 변경 13 | WORKDIR /app 14 | COPY . /app 15 | 16 | # 종속성 설치 (필요 시 only production 사용) 17 | RUN pip install --no-cache-dir -r requirements.txt 18 | 19 | # 사용자 변경 및 실행 20 | USER appuser 21 | CMD ["python", "app.py"] 22 | ``` 23 | 24 | ## 🔒 2. 종속성 검증 및 최신 버전 유지 25 | - **패키지 취약점 검사:** `pip-audit` 사용 26 | ```bash 27 | pip install pip-audit 28 | pip-audit 29 | ``` 30 | 31 | ## 📑 3. 다중 스테이지 빌드 (Multi-Stage Build) 32 | - **빌드와 실행 환경 분리:** 33 | ```dockerfile 34 | # 빌드 단계 35 | FROM python:3.11-slim AS builder 36 | WORKDIR /app 37 | COPY . /app 38 | RUN pip install --no-cache-dir -r requirements.txt 39 | 40 | # 실행 단계 41 | FROM python:3.11-slim 42 | WORKDIR /app 43 | COPY --from=builder /app . 44 | USER appuser 45 | CMD ["python", "app.py"] 46 | ``` 47 | 48 | ## 🔥 4. 불필요한 권한 제거 (Capabilities 및 Privileges) 49 | ```bash 50 | docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE 51 | ``` 52 | 53 | ## 📶 5. 네트워크 보안 및 방화벽 54 | ```bash 55 | docker network create --driver bridge secure_network 56 | docker run --network secure_network 57 | ``` 58 | 59 | ## 📜 6. 환경변수 보안 및 `.env` 관리 60 | ```dockerignore 61 | .env 62 | __pycache__ 63 | ``` 64 | ```bash 65 | docker run --env-file .env 66 | ``` 67 | 68 | ## 🔗 7. 이미지 서명 및 검증 69 | ```bash 70 | export DOCKER_CONTENT_TRUST=1 71 | ``` 72 | 73 | ## 🛡️ 8. CVE 점검 및 보안 스캐닝 74 | ```bash 75 | trivy image 76 | clairctl analyze 77 | ``` 78 | 79 | ## 📂 9. 파일 시스템 및 경로 보호 80 | ```bash 81 | docker run --read-only 82 | ``` 83 | ```bash 84 | docker run --read-only -v /data/app:rw 85 | ``` 86 | 87 | ## ✅ 보안 체크리스트 요약 88 | - [x] 루트 사용자 실행 방지 (`USER` 명령어 적용) 89 | - [x] 최소한의 커널 권한 사용 (`--cap-drop=ALL`) 90 | - [x] 다중 스테이지 빌드 사용 91 | - [x] 최신 보안 패키지 (`pip-audit`) 92 | - [x] 환경변수 파일 보호 (`.env` 관리) 93 | - [x] 취약점 검사 (`trivy`, `clair`) 94 | - [x] 이미지 서명 (`DOCKER_CONTENT_TRUST`) 95 | - [x] 읽기 전용 파일시스템 (`--read-only`) 96 | -------------------------------------------------------------------------------- /23.cloud/1..setup-aws-ecr/1.awscli.txt: -------------------------------------------------------------------------------- 1 | # Basic AWS CLIs 2 | # https://docs.aws.amazon.com/cli/latest/userguide/welcome-examples.html 3 | 4 | sudo apt install awscli 5 | 6 | aws --version 7 | 8 | aws configure 9 | - AWS Access Key ID 10 | - AWS Secret Access Key 11 | - Default region name 12 | 13 | 14 | # Multiple profile 15 | # https://docs.aws.amazon.com/ko_kr/cli/latest/userguide/cli-configure-profiles.html 16 | 17 | aws configure --profile bob 18 | 19 | 20 | # check output 21 | 22 | json, text, table 23 | 24 | aws ec2 describe-regions --output text 25 | aws ec2 describe-regions --output table 26 | aws ec2 describe-regions --output json 27 | 28 | 29 | 30 | # ec2 31 | # https://docs.aws.amazon.com/ko_kr/cli/latest/reference/ec2/describe-instances.html 32 | 33 | aws ec2 describe-instances 34 | 35 | aws ec2 create-key-pair --key-name my-keypair --query 'KeyMaterial' --output text > my-keypair.pem 36 | 37 | aws ec2 create-security-group --group-name my-group-sg --description "Security group for my instance" 38 | 39 | aws ec2 authorize-security-group-ingress --group-name my-group-sg --protocol tcp --port 22 --cidr 0.0.0.0/0 40 | 41 | aws ec2 run-instances --image-id ami-0454bb2fefc7de534 --count 1 --instance-type t2.micro --key-name my-keypair --security-groups my-group-sg 42 | 43 | aws ec2 describe-instances 44 | 45 | aws ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId,State.Name,LaunchTime,PublicIpAddress]' 46 | 47 | aws ec2 describe-instances --query 'Reservations[*].Instances[*].{Name:Tags[?Key==`Name`]|[0].Value,Id:InstanceId,State:State.Name,Launch:LaunchTime,PublicIP:PublicIpAddress,Key:SecurityGroups[0].GroupName}' --output table 48 | 49 | 50 | 51 | # iam 52 | # https://docs.aws.amazon.com/cli/latest/reference/iam/ 53 | 54 | aws iam create-user --user-name my-user 55 | 56 | aws iam create-group --group-name my-group 57 | 58 | aws iam add-user-to-group --user-name my-user --group-name my-group 59 | 60 | aws iam get-group --group-name my-group 61 | 62 | cat > my-policy.json << EOF 63 | { 64 | "Version": "2012-10-17", 65 | "Statement": [ 66 | { 67 | "Effect": "Allow", 68 | "NotAction": "iam:", 69 | "Resource": "*" 70 | } 71 | ] 72 | } 73 | EOF 74 | 75 | aws iam put-user-policy --user-name my-user --policy-name my-power-user-role --policy-document file://my-policy.json 76 | 77 | aws iam list-user-policies --user-name my-user 78 | 79 | aws iam create-login-profile --user-name my-user --password "abcd1234!" 80 | aws iam update-login-profile --user-name my-user --password "abcd1234@" 81 | 82 | aws iam create-access-key --user-name my-user 83 | 84 | 85 | ## clean up the created iam user 86 | # https://docs.aws.amazon.com/ko_kr/IAM/latest/UserGuide/id_users_manage.html#id_users_deleting_cli 87 | 88 | aws iam delete-user --user-name my-user 89 | 90 | aws iam list-access-keys 91 | 92 | aws iam list-access-keys --user-name my-user 93 | 94 | aws iam delete-access-key --user-name my-user --access-key-id XXXXXXXX 95 | 96 | aws iam delete-login-profile --user-name my-user 97 | 98 | aws iam list-user-policies --user-name my-user 99 | 100 | aws iam delete-user-policy --user-name my-user --policy my-power-user-role 101 | 102 | aws iam list-groups-for-user --user-name my-user 103 | 104 | aws iam remove-user-from-group --user-name my-user --group-name my-group 105 | 106 | aws iam delete-user --user-name my-user 107 | 108 | aws iam delete-group --group-name my-group 109 | 110 | 111 | 112 | ## ECS 용 필요한 권한 설정 113 | 114 | ### 그룹 자체에 줄 경우 (ECR + ECS 추가 및 삭제 명령어) 115 | 116 | aws iam list-groups 117 | 118 | aws iam list-attached-group-policies --group-name BoB10 119 | 120 | aws iam attach-group-policy --group-name BoB10 --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess 121 | aws iam attach-group-policy --group-name BoB10 --policy-arn arn:aws:iam::aws:policy/AmazonECS_FullAccess 122 | 123 | aws iam detach-group-policy --group-name BoB10 --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess 124 | aws iam detach-group-policy --group-name BoB10 --policy-arn arn:aws:iam::aws:policy/AmazonECS_FullAccess 125 | 126 | 127 | ### 개별 사용자에게 줄 경우 (ECS 만 예시로...) 128 | 129 | aws iam attach-user-policy --user-name ecs-user --policy-arn arn:aws:iam::aws:policy/AmazonECS_FullAccess 130 | 131 | aws iam detach-user-policy --user-name ecs-user --policy-arn arn:aws:iam::aws:policy/AmazonECS_FullAccess 132 | 133 | 134 | 135 | ## ECS 용 필요한 Role 생성 136 | # https://docs.aws.amazon.com/ko_kr/AmazonECS/latest/developerguide/task_execution_IAM_role.html 137 | 138 | aws iam list-roles 139 | 140 | cat > ecs-tasks-trust-policy.json << EOF 141 | { 142 | "Version": "2012-10-17", 143 | "Statement": [ 144 | { 145 | "Sid": "", 146 | "Effect": "Allow", 147 | "Principal": { 148 | "Service": "ecs-tasks.amazonaws.com" 149 | }, 150 | "Action": "sts:AssumeRole" 151 | } 152 | ] 153 | } 154 | EOF 155 | 156 | aws iam create-role --role-name ecsTaskExecutionRole --assume-role-policy-document file://ecs-tasks-trust-policy.json 157 | 158 | aws iam attach-role-policy --role-name ecsTaskExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy 159 | 160 | aws iam get-role --role-name ecsTaskExecutionRole 161 | 162 | 163 | ## Role(역할) 이란? 164 | - AWS 서비스를 요청하기 위한 "권한 세트" 를 정의하는 IAM 기능. 165 | - 역할의 큰 특징은 IAM 사용자나 IAM 그룹에는 연결되지 않는다는 것. 166 | - 대신 신뢰할 수 있는 IAM 사용자나 애플리케이션 또는 AWS 서비스(예: EC2)가 역할을 맡을 수 있다. 167 | 168 | - 역할을 맡게 되는 과정을 "임시 보안 자격 증명" 이라고 한다. 169 | 170 | 171 | aws s3 ls 172 | An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied 173 | 174 | aws sts assume-role --role-arn arn:aws:iam::7805xxxxxxxx:role/S3BucketListReadOnlyRole --role-session-name "my-s3-access-role" 175 | An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::7805xxxxxxxx:user/xxxxxxxx is not authorized to perform: sts:AssumeRole on resource: 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /23.cloud/1..setup-aws-ecr/2.ecrecs.txt: -------------------------------------------------------------------------------- 1 | # AWS 리포 생성 2 | 3 | ===== ECR ===== 4 | 5 | # ECR 리포지토리 생성 6 | # 앱 이름을 정하고 (tutorial-flask-app) 각 AWS 계정 ARN 을 기반으로 리포명이 정해져 있음 7 | # 해당 앱 이름을 만들면 그 후로는 tag 버전만이 붙어서 올라감 8 | 9 | xxxxxxxxxxxx.dkr.ecr.ap-northeast-2.amazonaws.com/tutorial-flask-app 10 | 11 | 12 | # 리포 접속 13 | 14 | ## AWS CLI 설정 15 | 16 | sudo apt install awscli 17 | aws configure 18 | - AWS Access Key ID 19 | - AWS Secret Access Key 20 | - Default region name 21 | 22 | 23 | ## AWS ECR 로그인 24 | aws ecr get-login --no-include-email 25 | - 위에 명령을 실행하면 docker login 으로 시작하는 명령어가 나옴 26 | - 해당 명령으로 docker login을 하면 24시간 동안 로그인이 유효함 27 | 28 | 29 | docker login -u AWS -p eyJwYXlsb2FkI...<중략>...czfQ== https://7805xxxxxxxx.dkr.ecr.ap-northeast-2.amazonaws.com 30 | 31 | docker tag flask-app:1.2 7805xxxxxxxx.dkr.ecr.ap-northeast-2.amazonaws.com/tutorial-flask-app:1.2 32 | 33 | docker push 7805xxxxxxxx.dkr.ecr.ap-northeast-2.amazonaws.com/tutorial-flask-app:1.2 34 | 35 | 36 | 37 | ===== ECS ===== 38 | 39 | # 개념 설명 40 | # https://aws.amazon.com/ko/ecs/features/?pg=ln&sec=gs 41 | # https://aws.amazon.com/ko/blogs/compute/building-blocks-of-amazon-ecs/ 42 | 43 | ## Cluster infra 44 | Fargate vs EC2 45 | - Fargate : Serverless container hosting 46 | - EC2 : Computing instance 47 | 48 | 49 | ## 구성 요소 50 | - Cluster : 배포 될 인프라 51 | - Task : 컨테이너 실행 최소 단위 (1개 혹은 1개 이상의 컨테이너) 52 | - Task definition : 컨테이너 실행 명세 53 | - Service : 작업을 묶은 단위 54 | 55 | 56 | ## 계층 구조 57 | Container definition < Task definition < Service < Cluster 58 | 59 | 60 | Task definition (작업 정의) 을 통해 ECS에서 실행될 작업 정의를 생성 (하나 또는 그 이상의 컨테이너 실행 정의) 61 | 62 | https://docs.aws.amazon.com/ko_kr/AmazonECS/latest/developerguide/getting-started-fargate.html 63 | 64 | 65 | EC2 -> Auto scaling Group -> ELB -> 배포 -> AMI 이미지 생성 -> Configuration 생성 -> Auto Scaling Group 연동... 66 | 67 | Fargate -> Cluster 생성 -> ECR 생성 -> Dockerfile 작성/수정 -> Image ECR에 Push -> Task definition -> 서비스 생성 68 | 69 | Fargate -> Cluster 생성 -> ECR 생성 -> Dockerfile 작성/수정 -> Image ECR에 Push -> Task definition -> 서비스 생성 -> ELB 생성 -> Update 서비스 70 | 71 | 72 | ## CLI 설정 명령 73 | - https://docs.aws.amazon.com/cli/latest/reference/ecs/index.html 74 | 75 | aws ecs list-clusters 76 | 77 | aws ecs list-tasks --cluster my-ecs-cluster 78 | 79 | 80 | aws ecs list-task-definitions 81 | 82 | aws ecs list-task-definition-families 83 | 84 | 85 | aws ecs list-services --cluster my-ecs-cluster 86 | 87 | 88 | aws ecs register-task-definition 89 | 90 | aws ecs deregister-task-definition 91 | 92 | 93 | aws ecs register-task-definition --container-definitions 94 | 95 | 96 | 97 | ## Hands-on python lab 98 | - https://hands-on.cloud/working-with-ecs-in-python-using-boto3/ 99 | - https://hands-on.cloud/working-with-ec2-instances-using-boto3-in-python/ 100 | 101 | 102 | 103 | # 요약 프로세스 104 | deploy_image 105 | - docker login -u -p 106 | - docker push 107 | 108 | deploy_cluster 109 | - make_task_def 110 | - setup json 111 | - register_definition 112 | - aws ecs register-task-definition --container-definitions xxxx-task-def --family xxxx 113 | - aws ecs update-service --cluster xxxx-ecs --service xxxx-ecs-service --task-definition xxxx 114 | - check for older version 115 | - aws ecs describe-services --cluster xxxx-ecs --services xxxx-ecs-service 116 | 117 | => task_template = [ 118 | { 119 | "name": "uwsgi", 120 | "image": "xxxxxxxx/xxxx-ecs:%s", 121 | "essential": true, 122 | "memory": 200, 123 | "cpu": 10 124 | }, 125 | { 126 | "name": "nginx", 127 | "links": [ 128 | "uwsgi" 129 | ], 130 | "image": "xxxxxxxx/xxxx-base:latest", 131 | "portMappings": [ 132 | { 133 | "containerPort": 8000, 134 | "hostPort": %s 135 | } 136 | ], 137 | "cpu": 10, 138 | "memory": 200, 139 | "essential": true 140 | } 141 | ] 142 | 143 | 144 | -------------------------------------------------------------------------------- /23.cloud/1..setup-aws-ecr/3.cicd.txt: -------------------------------------------------------------------------------- 1 | 2 | # CI 와 연동 3 | 4 | ## AWS ECR - Travis CI 5 | 6 | after_success: 7 | # Install AWS CLI 8 | - pip install --user awscli # install aws cli w/o sudo 9 | - export PATH=$PATH:$HOME/.local/bin # put aws in the path 10 | # Log in to the docker CLI 11 | - eval $(aws ecr get-login --region us-east-1) # needs AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY envvars 12 | # Build docker image 13 | - docker build -t dockerhub_id/image_name ./ 14 | # Tag image for AWS ECR 15 | - docker tag dockerhub_id/image_name:latest ECR_URI/dockerhub_id/image_name:latest 16 | # Take those image and push them to docker hub 17 | - docker push dockerhub_id/image_name:latest 18 | 19 | 20 | # Docker Hub - Travis CI 21 | 22 | after_success: 23 | # Log in to the docker CLI 24 | - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_ID" --password-stdin 25 | # Biuld docker image 26 | - docker build -t dockerhub_id/image_name ./ 27 | # Take those image and push them to docker hub 28 | - docker push dockerhub_id/image_name:latest 29 | 30 | 31 | 32 | # AWS ECR actions 33 | # https://github.com/aws-actions/amazon-ecr-login 34 | 35 | 36 | -------------------------------------------------------------------------------- /23.cloud/10.billing/aws-cost-usage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import boto3 5 | import datetime 6 | 7 | 8 | def parse_arguments(): 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('--days', type=int, default=90) 11 | parser.add_argument('--profile', type=str, default="default") 12 | parser.add_argument('--resolution', type=str, default="MONTHLY") 13 | args = parser.parse_args() 14 | 15 | return args 16 | 17 | 18 | def process_dates(args): 19 | now = datetime.datetime.utcnow() 20 | if args.resolution == 'MONTHLY': 21 | start = (now - datetime.timedelta(days=args.days)).strftime('%Y-%m-01') 22 | else: 23 | start = (now - datetime.timedelta(days=args.days)).strftime('%Y-%m-%d') 24 | end = now.strftime('%Y-%m-%d') 25 | 26 | return start, end 27 | 28 | 29 | def retrieve_from_aws(args, start, end): 30 | session = boto3.session.Session(profile_name=args.profile) 31 | ce_client = session.client('ce', 'us-east-1') 32 | 33 | results = [] 34 | token = None 35 | 36 | while True: 37 | if token: 38 | kwargs = {'NextPageToken': token} 39 | else: 40 | kwargs = {} 41 | 42 | data = ce_client.get_cost_and_usage( 43 | TimePeriod = {'Start': start, 'End': end}, 44 | Granularity = args.resolution, 45 | Metrics = ['UnblendedCost'], 46 | GroupBy = [{'Type': 'DIMENSION', 'Key': 'LINKED_ACCOUNT'}, {'Type': 'DIMENSION', 'Key': 'SERVICE'}], 47 | **kwargs) 48 | 49 | results += data['ResultsByTime'] 50 | token = data.get('NextPageToken') 51 | if not token: 52 | break 53 | 54 | return results 55 | 56 | 57 | def parse_results(results): 58 | print('\t'.join(['TimePeriod', 'Amount', 'Unit', 'Est.', 'LinkedAccount', 'Service'])) 59 | for result_by_time in results: 60 | sum = 0 61 | for group in result_by_time['Groups']: 62 | amount = float(group['Metrics']['UnblendedCost']['Amount']) 63 | unit = group['Metrics']['UnblendedCost']['Unit'] 64 | services = '\t'.join(group['Keys']) 65 | 66 | print("{}\t{:7.2f}\t{}\t{}\t{}".format( 67 | result_by_time['TimePeriod']['Start'], 68 | amount, 69 | unit, 70 | result_by_time['Estimated'], 71 | services)) 72 | sum += amount 73 | print('TOTAL:\t{:7.2f}'.format(sum)) 74 | print('-'*20) 75 | 76 | 77 | def main(): 78 | args = parse_arguments() 79 | start, end = process_dates(args) 80 | results = retrieve_from_aws(args, start, end) 81 | parse_results(results) 82 | 83 | 84 | if __name__ == "__main__": 85 | main() 86 | -------------------------------------------------------------------------------- /23.cloud/9.cleanup-aws/delete_all_ec2.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | 4 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html 5 | 6 | def setup_bob_track(track=None): 7 | dev_session = boto3.session.Session(profile_name='kitribob.dev') 8 | vapt_session = boto3.session.Session(profile_name='kitribob.vapt') 9 | consult_session = boto3.session.Session(profile_name='kitribob.consult') 10 | df_session = boto3.session.Session(profile_name='kitribob.df') 11 | 12 | # boto3.setup_default_session(profile_name='kitribob.vapt') 13 | 14 | if track == "dev": 15 | current_session = dev_session 16 | if track == "vapt": 17 | current_session = vapt_session 18 | if track == "consult": 19 | current_session = consult_session 20 | if track == "df": 21 | current_session = df_session 22 | 23 | if track is None: 24 | raise ValueError 25 | 26 | return current_session 27 | 28 | 29 | def region_check(current_session, region=["ALL"]): 30 | ec2_client = current_session.client('ec2') 31 | all_regions = ec2_client.describe_regions() 32 | 33 | region_boundary = [] 34 | if region[0] == "ALL": 35 | for r in all_regions['Regions']: 36 | region_boundary.append(r['RegionName']) 37 | else: 38 | region_boundary = region 39 | 40 | return region_boundary 41 | 42 | 43 | def list_ec2_lowlevel(current_session, region=["ALL"]): 44 | region_boundary = region_check(current_session, region) 45 | print('=== EC2 List instances ===') 46 | for region_name in region_boundary: 47 | print(f'region_name: {region_name}') 48 | ec2 = current_session.resource('ec2', region_name=region_name) 49 | instances = ec2.meta.client.describe_instances() 50 | for instance in instances['Reservations']: 51 | for inst in instance['Instances']: 52 | if "InstanceId" in inst: 53 | print(inst['InstanceId'], end=" ") 54 | 55 | if "KeyName" in inst: 56 | print(inst['KeyName'], end=" ") 57 | else: 58 | print('-', end=" ") 59 | 60 | if "State" in inst: 61 | print(inst['State']) 62 | 63 | 64 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.ServiceResource.instances 65 | def list_ec2(current_session, region=["ALL"]): 66 | region_boundary = region_check(current_session, region) 67 | print('=== EC2 List instances ===') 68 | for region_name in region_boundary: 69 | print(f'region_name: {region_name}') 70 | ec2 = current_session.resource('ec2', region_name=region_name) 71 | for instance in ec2.instances.all(): 72 | # print(instance) 73 | print("Id: {0}, Type: {1}, PublicIP: {2}, State: {3}".format( 74 | instance.id, instance.instance_type, instance.public_ip_address, instance.state)) 75 | 76 | 77 | def list_keypairs(current_session, region="ALL"): 78 | region_boundary = region_check(current_session, region) 79 | print('=== EC2 List keypairs ===') 80 | for region_name in region_boundary: 81 | print(f'region_name: {region_name}') 82 | ec2 = current_session.resource('ec2', region_name=region_name) 83 | for keypairs in ec2.key_pairs.all(): 84 | # print(keypairs) 85 | print("Name: {0}".format(keypairs.key_name)) 86 | 87 | 88 | if __name__ == "__main__": 89 | client = setup_bob_track("dev") 90 | # REGION=['ALL'] 91 | REGION=['ap-northeast-2'] 92 | list_ec2(client, region=REGION) 93 | list_keypairs(client, region=REGION) 94 | -------------------------------------------------------------------------------- /23.cloud/9.cleanup-aws/delete_all_ecr.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | 4 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecr.html 5 | 6 | def setup_bob_track(track=None): 7 | dev_session = boto3.session.Session(profile_name='kitribob.dev') 8 | vapt_session = boto3.session.Session(profile_name='kitribob.vapt') 9 | consult_session = boto3.session.Session(profile_name='kitribob.consult') 10 | df_session = boto3.session.Session(profile_name='kitribob.df') 11 | 12 | if track == "dev": 13 | current_session = dev_session 14 | if track == "vapt": 15 | current_session = vapt_session 16 | if track == "consult": 17 | current_session = consult_session 18 | if track == "df": 19 | current_session = df_session 20 | 21 | if track is None: 22 | raise ValueError 23 | 24 | return current_session 25 | 26 | 27 | def list_ecr(current_session): 28 | ecr_client = current_session.client('ecr') 29 | response = ecr_client.describe_repositories() 30 | 31 | print('=== ECR ===') 32 | for repository in response['repositories']: 33 | print(" ", repository['repositoryArn']) 34 | 35 | 36 | def delete_ecr(current_session, force=False): 37 | ecr_client = current_session.client('ecr') 38 | response = ecr_client.describe_repositories() 39 | 40 | print('=== ECR Repos ===') 41 | for repository in response['repositories']: 42 | print(repository['repositoryArn']) 43 | repo_name = repository['repositoryName'] 44 | print(' + List ECR repo images') 45 | images = ecr_client.list_images(repositoryName=repo_name) 46 | imageTags = [tag['imageTag'] for tag in images['imageIds']] 47 | print(" ", imageTags) 48 | print(' - Delete ECR repository, ', repo_name) 49 | ecr_client.delete_repository(repositoryName=repo_name, force=force) 50 | 51 | 52 | if __name__ == "__main__": 53 | client = setup_bob_track("dev") 54 | list_ecr(client) 55 | delete_ecr(client, force=True) 56 | -------------------------------------------------------------------------------- /23.cloud/9.cleanup-aws/delete_all_ecs.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | 4 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html 5 | 6 | def setup_bob_track(track=None): 7 | dev_session = boto3.session.Session(profile_name='kitribob.dev') 8 | vapt_session = boto3.session.Session(profile_name='kitribob.vapt') 9 | consult_session = boto3.session.Session(profile_name='kitribob.consult') 10 | df_session = boto3.session.Session(profile_name='kitribob.df') 11 | 12 | if track == "dev": 13 | current_session = dev_session 14 | if track == "vapt": 15 | current_session = vapt_session 16 | if track == "consult": 17 | current_session = consult_session 18 | if track == "df": 19 | current_session = df_session 20 | 21 | if track is None: 22 | raise ValueError 23 | 24 | return current_session 25 | 26 | 27 | def list_ecs(current_session): 28 | print('=== ECS ===') 29 | ecs_client = current_session.client('ecs') 30 | paginator = ecs_client.get_paginator('list_clusters') 31 | page_iterator = paginator.paginate() 32 | for page in page_iterator: 33 | for cluster_arn in page['clusterArns']: 34 | print(" ", cluster_arn) 35 | 36 | 37 | def delete_ecs(current_session): 38 | print('=== ECS ===') 39 | ecs_client = current_session.client('ecs') 40 | paginator = ecs_client.get_paginator('list_clusters') 41 | page_iterator = paginator.paginate() 42 | for page in page_iterator: 43 | for cluster_arn in page['clusterArns']: 44 | cluster_name = cluster_arn.split('/')[1] 45 | print(cluster_name) 46 | 47 | print(' + ECS list services') 48 | paginator = ecs_client.get_paginator('list_services') 49 | page_iterator = paginator.paginate(cluster=cluster_name) 50 | for page in page_iterator: 51 | for service_arn in page['serviceArns']: 52 | print(" ", service_arn) 53 | service_name = service_arn.split('/')[-1] 54 | print(' - ECS delete services, ', service_name) 55 | ecs_client.update_service(cluster=cluster_name, service=service_name, desiredCount=0) 56 | ecs_client.delete_service(cluster=cluster_name, service=service_name) 57 | 58 | print(' + ECS list tasks') 59 | paginator = ecs_client.get_paginator('list_tasks') 60 | page_iterator = paginator.paginate(cluster=cluster_name) 61 | for page in page_iterator: 62 | for task_arn in page['taskArns']: 63 | print(" ", task_arn) 64 | task_name = task_arn.split('/')[-1] 65 | print(' - ECS delete tasks', task_name) 66 | ecs_client.stop_task(cluster=cluster_name, task=task_arn) 67 | 68 | print(' + ECS list containers') 69 | paginator = ecs_client.get_paginator('list_container_instances') 70 | page_iterator = paginator.paginate(cluster=cluster_name) 71 | for page in page_iterator: 72 | for container_arn in page['containerInstanceArns']: 73 | print(" ", container_arn) 74 | container_name = container_arn.split('/')[-1] 75 | print(' - ECS delete container instance', container_name) 76 | ecs_client.deregister_container_instance(cluster=cluster_name, containerInstance=container_arn) 77 | 78 | 79 | def list_ecs_task_definition(current_session): 80 | ecs_client = current_session.client('ecs') 81 | print('=== ECS Task Definition Families ===') 82 | paginator = ecs_client.get_paginator('list_task_definition_families') 83 | response_iterator = paginator.paginate() 84 | for each_page in response_iterator: 85 | print(each_page['families']) 86 | 87 | 88 | def delete_ecs_task_definition(current_session): 89 | ecs_client = current_session.client('ecs') 90 | print('=== ECS Task Definitions ===') 91 | paginator = ecs_client.get_paginator('list_task_definitions') 92 | response_iterator = paginator.paginate() 93 | for each_page in response_iterator: 94 | for each_task in each_page['taskDefinitionArns']: 95 | print(each_task) 96 | print(' - DELETE taskDefinitionArns, ', each_task) 97 | response = ecs_client.deregister_task_definition(taskDefinition=each_task) 98 | # print(json.dumps(response, indent=4, default=str)) 99 | print(" ", response['taskDefinition']['taskDefinitionArn'], response['ResponseMetadata']['HTTPStatusCode']) 100 | 101 | 102 | def delete_ecs_clusters(current_session): 103 | ecs_client = current_session.client('ecs') 104 | print('=== DELETE ECS Clusters ===') 105 | paginator = ecs_client.get_paginator('list_clusters') 106 | page_iterator = paginator.paginate() 107 | for page in page_iterator: 108 | for cluter_arn in page['clusterArns']: 109 | cluster_name = cluter_arn.split('/')[1] 110 | print(' - DELETE cluters, ', cluster_name) 111 | response = ecs_client.delete_cluster(cluster=cluster_name) 112 | # print(json.dumps(response, indent=4)) 113 | print(" ", response['cluster']['clusterArn'], response['ResponseMetaata']['HTTPStatusCode']) 114 | 115 | 116 | if __name__ == "__main__": 117 | client = setup_bob_track("dev") 118 | list_ecs(client) 119 | delete_ecs(client) 120 | list_ecs_task_definition(client) 121 | delete_ecs_task_definition(client) 122 | delete_ecs_clusters(client) 123 | -------------------------------------------------------------------------------- /23.cloud/README.md: -------------------------------------------------------------------------------- 1 | # 1.setup-aws-ecr 2 | aws cli basics 3 | ecr ecs setup 4 | -------------------------------------------------------------------------------- /3.flask/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | __pycache__ 4 | -------------------------------------------------------------------------------- /3.flask/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | 3 | RUN pip install flask 4 | 5 | WORKDIR /app 6 | COPY . . 7 | 8 | CMD ["python", "app.py"] 9 | 10 | EXPOSE 5000 11 | -------------------------------------------------------------------------------- /3.flask/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route('/') 6 | def main(): 7 | return 'hello' 8 | 9 | if __name__ == "__main__": 10 | app.run(host='0.0.0.0', port=5000) 11 | -------------------------------------------------------------------------------- /4.flask/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | __pycache__ 4 | README.md -------------------------------------------------------------------------------- /4.flask/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | 3 | # RUN apk add --update curl && \ 4 | # rm -rf /var/cache/apk/* 5 | 6 | # RUN pip install flask 7 | 8 | WORKDIR /app 9 | COPY . /app 10 | 11 | RUN pip install -r requirements.txt 12 | 13 | ENTRYPOINT ["python", "app.py"] 14 | 15 | # Using unbuffered output to see the python "print" in "docker logs my-app" 16 | # ENTRYPOINT ["python", "-u", "app.py"] 17 | # or -e PYTHONUNBUFFERED=1 with docker cmd 18 | 19 | EXPOSE 5000 20 | -------------------------------------------------------------------------------- /4.flask/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, render_template 3 | from werkzeug.middleware.proxy_fix import ProxyFix 4 | 5 | app = Flask(__name__) 6 | app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) 7 | 8 | color = os.environ.get('APP_COLOR') 9 | 10 | @app.route('/') 11 | def main(): 12 | return render_template('hello.html', color=color) 13 | 14 | if __name__ == "__main__": 15 | app.run(host='0.0.0.0', port=5000) 16 | -------------------------------------------------------------------------------- /4.flask/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.0.1 2 | -------------------------------------------------------------------------------- /4.flask/templates/hello.html: -------------------------------------------------------------------------------- 1 | 2 | Hello Flask 3 | 4 |
5 |

Hello, World!

6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /5.express/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | node_modules 4 | README.txt 5 | -------------------------------------------------------------------------------- /5.express/Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM node:alpine 2 | FROM node:10-alpine 3 | 4 | RUN apk --no-cache add curl 5 | 6 | WORKDIR /app 7 | 8 | # ADD app.js /app 9 | # ADD package.json /app 10 | COPY . . 11 | 12 | RUN npm install 13 | # RUN npm install --production 14 | # RUN npm clean-install 15 | 16 | CMD ["node", "app.js"] 17 | -------------------------------------------------------------------------------- /5.express/README.txt: -------------------------------------------------------------------------------- 1 | # 이미지 빌드 2 | docker build . -t my-express-app:1.0 3 | 4 | # 노드 빌드 환경 5 | # 개발 환경 패키지 추가 (--save-dev, -D) 6 | npm install --save-dev nodemon 7 | 8 | "scripts": { 9 | "start": "nodemon app.js" 10 | } 11 | 12 | npm start 13 | 14 | 15 | # 설치 시 (개발도구 제외) 16 | npm install --production 17 | 18 | 19 | # 추가로 package-lock.json 을 참고해서 정확한(동일한) 버전을 재설치 시 20 | npm clean-install 21 | -------------------------------------------------------------------------------- /5.express/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | 4 | const os = require('os'); 5 | const port = 3000; 6 | 7 | app.get('/', function (req,res) { 8 | // res.send('Hello Express\n'); 9 | 10 | // ip = req.connection.remoteAddress; // 예전 방식, express4 부터는 req.ip 로 단축 11 | ip = req.ip; 12 | // ip = req.headers['x-forwarded-for'] || req.ip; 13 | 14 | console.log(`Received request from ${ip}`); 15 | res.send(`

Welcome to ${os.hostname()}

\n`); 16 | // res.send(`

Welcome to ${os.hostname()}

\n`); 17 | }); 18 | 19 | app.listen(port, () => { 20 | console.log(`Express is ready at http://localhost:${port}`); 21 | // console.log(`Express is ready at http://${os.hostname}:${port}`); 22 | }); 23 | -------------------------------------------------------------------------------- /5.express/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5.express", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "5.express", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.18.2" 13 | } 14 | }, 15 | "node_modules/accepts": { 16 | "version": "1.3.8", 17 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 18 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 19 | "dependencies": { 20 | "mime-types": "~2.1.34", 21 | "negotiator": "0.6.3" 22 | }, 23 | "engines": { 24 | "node": ">= 0.6" 25 | } 26 | }, 27 | "node_modules/array-flatten": { 28 | "version": "1.1.1", 29 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 30 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 31 | }, 32 | "node_modules/body-parser": { 33 | "version": "1.20.1", 34 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 35 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 36 | "dependencies": { 37 | "bytes": "3.1.2", 38 | "content-type": "~1.0.4", 39 | "debug": "2.6.9", 40 | "depd": "2.0.0", 41 | "destroy": "1.2.0", 42 | "http-errors": "2.0.0", 43 | "iconv-lite": "0.4.24", 44 | "on-finished": "2.4.1", 45 | "qs": "6.11.0", 46 | "raw-body": "2.5.1", 47 | "type-is": "~1.6.18", 48 | "unpipe": "1.0.0" 49 | }, 50 | "engines": { 51 | "node": ">= 0.8", 52 | "npm": "1.2.8000 || >= 1.4.16" 53 | } 54 | }, 55 | "node_modules/bytes": { 56 | "version": "3.1.2", 57 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 58 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 59 | "engines": { 60 | "node": ">= 0.8" 61 | } 62 | }, 63 | "node_modules/call-bind": { 64 | "version": "1.0.5", 65 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", 66 | "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", 67 | "dependencies": { 68 | "function-bind": "^1.1.2", 69 | "get-intrinsic": "^1.2.1", 70 | "set-function-length": "^1.1.1" 71 | }, 72 | "funding": { 73 | "url": "https://github.com/sponsors/ljharb" 74 | } 75 | }, 76 | "node_modules/content-disposition": { 77 | "version": "0.5.4", 78 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 79 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 80 | "dependencies": { 81 | "safe-buffer": "5.2.1" 82 | }, 83 | "engines": { 84 | "node": ">= 0.6" 85 | } 86 | }, 87 | "node_modules/content-type": { 88 | "version": "1.0.5", 89 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 90 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 91 | "engines": { 92 | "node": ">= 0.6" 93 | } 94 | }, 95 | "node_modules/cookie": { 96 | "version": "0.5.0", 97 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 98 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 99 | "engines": { 100 | "node": ">= 0.6" 101 | } 102 | }, 103 | "node_modules/cookie-signature": { 104 | "version": "1.0.6", 105 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 106 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 107 | }, 108 | "node_modules/debug": { 109 | "version": "2.6.9", 110 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 111 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 112 | "dependencies": { 113 | "ms": "2.0.0" 114 | } 115 | }, 116 | "node_modules/define-data-property": { 117 | "version": "1.1.1", 118 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", 119 | "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", 120 | "dependencies": { 121 | "get-intrinsic": "^1.2.1", 122 | "gopd": "^1.0.1", 123 | "has-property-descriptors": "^1.0.0" 124 | }, 125 | "engines": { 126 | "node": ">= 0.4" 127 | } 128 | }, 129 | "node_modules/depd": { 130 | "version": "2.0.0", 131 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 132 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 133 | "engines": { 134 | "node": ">= 0.8" 135 | } 136 | }, 137 | "node_modules/destroy": { 138 | "version": "1.2.0", 139 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 140 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 141 | "engines": { 142 | "node": ">= 0.8", 143 | "npm": "1.2.8000 || >= 1.4.16" 144 | } 145 | }, 146 | "node_modules/ee-first": { 147 | "version": "1.1.1", 148 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 149 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 150 | }, 151 | "node_modules/encodeurl": { 152 | "version": "1.0.2", 153 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 154 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 155 | "engines": { 156 | "node": ">= 0.8" 157 | } 158 | }, 159 | "node_modules/escape-html": { 160 | "version": "1.0.3", 161 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 162 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 163 | }, 164 | "node_modules/etag": { 165 | "version": "1.8.1", 166 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 167 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 168 | "engines": { 169 | "node": ">= 0.6" 170 | } 171 | }, 172 | "node_modules/express": { 173 | "version": "4.18.2", 174 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 175 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 176 | "dependencies": { 177 | "accepts": "~1.3.8", 178 | "array-flatten": "1.1.1", 179 | "body-parser": "1.20.1", 180 | "content-disposition": "0.5.4", 181 | "content-type": "~1.0.4", 182 | "cookie": "0.5.0", 183 | "cookie-signature": "1.0.6", 184 | "debug": "2.6.9", 185 | "depd": "2.0.0", 186 | "encodeurl": "~1.0.2", 187 | "escape-html": "~1.0.3", 188 | "etag": "~1.8.1", 189 | "finalhandler": "1.2.0", 190 | "fresh": "0.5.2", 191 | "http-errors": "2.0.0", 192 | "merge-descriptors": "1.0.1", 193 | "methods": "~1.1.2", 194 | "on-finished": "2.4.1", 195 | "parseurl": "~1.3.3", 196 | "path-to-regexp": "0.1.7", 197 | "proxy-addr": "~2.0.7", 198 | "qs": "6.11.0", 199 | "range-parser": "~1.2.1", 200 | "safe-buffer": "5.2.1", 201 | "send": "0.18.0", 202 | "serve-static": "1.15.0", 203 | "setprototypeof": "1.2.0", 204 | "statuses": "2.0.1", 205 | "type-is": "~1.6.18", 206 | "utils-merge": "1.0.1", 207 | "vary": "~1.1.2" 208 | }, 209 | "engines": { 210 | "node": ">= 0.10.0" 211 | } 212 | }, 213 | "node_modules/finalhandler": { 214 | "version": "1.2.0", 215 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 216 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 217 | "dependencies": { 218 | "debug": "2.6.9", 219 | "encodeurl": "~1.0.2", 220 | "escape-html": "~1.0.3", 221 | "on-finished": "2.4.1", 222 | "parseurl": "~1.3.3", 223 | "statuses": "2.0.1", 224 | "unpipe": "~1.0.0" 225 | }, 226 | "engines": { 227 | "node": ">= 0.8" 228 | } 229 | }, 230 | "node_modules/forwarded": { 231 | "version": "0.2.0", 232 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 233 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 234 | "engines": { 235 | "node": ">= 0.6" 236 | } 237 | }, 238 | "node_modules/fresh": { 239 | "version": "0.5.2", 240 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 241 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 242 | "engines": { 243 | "node": ">= 0.6" 244 | } 245 | }, 246 | "node_modules/function-bind": { 247 | "version": "1.1.2", 248 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 249 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 250 | "funding": { 251 | "url": "https://github.com/sponsors/ljharb" 252 | } 253 | }, 254 | "node_modules/get-intrinsic": { 255 | "version": "1.2.2", 256 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", 257 | "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", 258 | "dependencies": { 259 | "function-bind": "^1.1.2", 260 | "has-proto": "^1.0.1", 261 | "has-symbols": "^1.0.3", 262 | "hasown": "^2.0.0" 263 | }, 264 | "funding": { 265 | "url": "https://github.com/sponsors/ljharb" 266 | } 267 | }, 268 | "node_modules/gopd": { 269 | "version": "1.0.1", 270 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 271 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 272 | "dependencies": { 273 | "get-intrinsic": "^1.1.3" 274 | }, 275 | "funding": { 276 | "url": "https://github.com/sponsors/ljharb" 277 | } 278 | }, 279 | "node_modules/has-property-descriptors": { 280 | "version": "1.0.1", 281 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", 282 | "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", 283 | "dependencies": { 284 | "get-intrinsic": "^1.2.2" 285 | }, 286 | "funding": { 287 | "url": "https://github.com/sponsors/ljharb" 288 | } 289 | }, 290 | "node_modules/has-proto": { 291 | "version": "1.0.1", 292 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 293 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 294 | "engines": { 295 | "node": ">= 0.4" 296 | }, 297 | "funding": { 298 | "url": "https://github.com/sponsors/ljharb" 299 | } 300 | }, 301 | "node_modules/has-symbols": { 302 | "version": "1.0.3", 303 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 304 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 305 | "engines": { 306 | "node": ">= 0.4" 307 | }, 308 | "funding": { 309 | "url": "https://github.com/sponsors/ljharb" 310 | } 311 | }, 312 | "node_modules/hasown": { 313 | "version": "2.0.0", 314 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", 315 | "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", 316 | "dependencies": { 317 | "function-bind": "^1.1.2" 318 | }, 319 | "engines": { 320 | "node": ">= 0.4" 321 | } 322 | }, 323 | "node_modules/http-errors": { 324 | "version": "2.0.0", 325 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 326 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 327 | "dependencies": { 328 | "depd": "2.0.0", 329 | "inherits": "2.0.4", 330 | "setprototypeof": "1.2.0", 331 | "statuses": "2.0.1", 332 | "toidentifier": "1.0.1" 333 | }, 334 | "engines": { 335 | "node": ">= 0.8" 336 | } 337 | }, 338 | "node_modules/iconv-lite": { 339 | "version": "0.4.24", 340 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 341 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 342 | "dependencies": { 343 | "safer-buffer": ">= 2.1.2 < 3" 344 | }, 345 | "engines": { 346 | "node": ">=0.10.0" 347 | } 348 | }, 349 | "node_modules/inherits": { 350 | "version": "2.0.4", 351 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 352 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 353 | }, 354 | "node_modules/ipaddr.js": { 355 | "version": "1.9.1", 356 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 357 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 358 | "engines": { 359 | "node": ">= 0.10" 360 | } 361 | }, 362 | "node_modules/media-typer": { 363 | "version": "0.3.0", 364 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 365 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 366 | "engines": { 367 | "node": ">= 0.6" 368 | } 369 | }, 370 | "node_modules/merge-descriptors": { 371 | "version": "1.0.1", 372 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 373 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 374 | }, 375 | "node_modules/methods": { 376 | "version": "1.1.2", 377 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 378 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", 379 | "engines": { 380 | "node": ">= 0.6" 381 | } 382 | }, 383 | "node_modules/mime": { 384 | "version": "1.6.0", 385 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 386 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 387 | "bin": { 388 | "mime": "cli.js" 389 | }, 390 | "engines": { 391 | "node": ">=4" 392 | } 393 | }, 394 | "node_modules/mime-db": { 395 | "version": "1.52.0", 396 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 397 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 398 | "engines": { 399 | "node": ">= 0.6" 400 | } 401 | }, 402 | "node_modules/mime-types": { 403 | "version": "2.1.35", 404 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 405 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 406 | "dependencies": { 407 | "mime-db": "1.52.0" 408 | }, 409 | "engines": { 410 | "node": ">= 0.6" 411 | } 412 | }, 413 | "node_modules/ms": { 414 | "version": "2.0.0", 415 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 416 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 417 | }, 418 | "node_modules/negotiator": { 419 | "version": "0.6.3", 420 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 421 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 422 | "engines": { 423 | "node": ">= 0.6" 424 | } 425 | }, 426 | "node_modules/object-inspect": { 427 | "version": "1.13.1", 428 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", 429 | "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", 430 | "funding": { 431 | "url": "https://github.com/sponsors/ljharb" 432 | } 433 | }, 434 | "node_modules/on-finished": { 435 | "version": "2.4.1", 436 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 437 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 438 | "dependencies": { 439 | "ee-first": "1.1.1" 440 | }, 441 | "engines": { 442 | "node": ">= 0.8" 443 | } 444 | }, 445 | "node_modules/parseurl": { 446 | "version": "1.3.3", 447 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 448 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 449 | "engines": { 450 | "node": ">= 0.8" 451 | } 452 | }, 453 | "node_modules/path-to-regexp": { 454 | "version": "0.1.7", 455 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 456 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 457 | }, 458 | "node_modules/proxy-addr": { 459 | "version": "2.0.7", 460 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 461 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 462 | "dependencies": { 463 | "forwarded": "0.2.0", 464 | "ipaddr.js": "1.9.1" 465 | }, 466 | "engines": { 467 | "node": ">= 0.10" 468 | } 469 | }, 470 | "node_modules/qs": { 471 | "version": "6.11.0", 472 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 473 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 474 | "dependencies": { 475 | "side-channel": "^1.0.4" 476 | }, 477 | "engines": { 478 | "node": ">=0.6" 479 | }, 480 | "funding": { 481 | "url": "https://github.com/sponsors/ljharb" 482 | } 483 | }, 484 | "node_modules/range-parser": { 485 | "version": "1.2.1", 486 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 487 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 488 | "engines": { 489 | "node": ">= 0.6" 490 | } 491 | }, 492 | "node_modules/raw-body": { 493 | "version": "2.5.1", 494 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 495 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 496 | "dependencies": { 497 | "bytes": "3.1.2", 498 | "http-errors": "2.0.0", 499 | "iconv-lite": "0.4.24", 500 | "unpipe": "1.0.0" 501 | }, 502 | "engines": { 503 | "node": ">= 0.8" 504 | } 505 | }, 506 | "node_modules/safe-buffer": { 507 | "version": "5.2.1", 508 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 509 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 510 | "funding": [ 511 | { 512 | "type": "github", 513 | "url": "https://github.com/sponsors/feross" 514 | }, 515 | { 516 | "type": "patreon", 517 | "url": "https://www.patreon.com/feross" 518 | }, 519 | { 520 | "type": "consulting", 521 | "url": "https://feross.org/support" 522 | } 523 | ] 524 | }, 525 | "node_modules/safer-buffer": { 526 | "version": "2.1.2", 527 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 528 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 529 | }, 530 | "node_modules/send": { 531 | "version": "0.18.0", 532 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 533 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 534 | "dependencies": { 535 | "debug": "2.6.9", 536 | "depd": "2.0.0", 537 | "destroy": "1.2.0", 538 | "encodeurl": "~1.0.2", 539 | "escape-html": "~1.0.3", 540 | "etag": "~1.8.1", 541 | "fresh": "0.5.2", 542 | "http-errors": "2.0.0", 543 | "mime": "1.6.0", 544 | "ms": "2.1.3", 545 | "on-finished": "2.4.1", 546 | "range-parser": "~1.2.1", 547 | "statuses": "2.0.1" 548 | }, 549 | "engines": { 550 | "node": ">= 0.8.0" 551 | } 552 | }, 553 | "node_modules/send/node_modules/ms": { 554 | "version": "2.1.3", 555 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 556 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 557 | }, 558 | "node_modules/serve-static": { 559 | "version": "1.15.0", 560 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 561 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 562 | "dependencies": { 563 | "encodeurl": "~1.0.2", 564 | "escape-html": "~1.0.3", 565 | "parseurl": "~1.3.3", 566 | "send": "0.18.0" 567 | }, 568 | "engines": { 569 | "node": ">= 0.8.0" 570 | } 571 | }, 572 | "node_modules/set-function-length": { 573 | "version": "1.2.0", 574 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", 575 | "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", 576 | "dependencies": { 577 | "define-data-property": "^1.1.1", 578 | "function-bind": "^1.1.2", 579 | "get-intrinsic": "^1.2.2", 580 | "gopd": "^1.0.1", 581 | "has-property-descriptors": "^1.0.1" 582 | }, 583 | "engines": { 584 | "node": ">= 0.4" 585 | } 586 | }, 587 | "node_modules/setprototypeof": { 588 | "version": "1.2.0", 589 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 590 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 591 | }, 592 | "node_modules/side-channel": { 593 | "version": "1.0.4", 594 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 595 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 596 | "dependencies": { 597 | "call-bind": "^1.0.0", 598 | "get-intrinsic": "^1.0.2", 599 | "object-inspect": "^1.9.0" 600 | }, 601 | "funding": { 602 | "url": "https://github.com/sponsors/ljharb" 603 | } 604 | }, 605 | "node_modules/statuses": { 606 | "version": "2.0.1", 607 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 608 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 609 | "engines": { 610 | "node": ">= 0.8" 611 | } 612 | }, 613 | "node_modules/toidentifier": { 614 | "version": "1.0.1", 615 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 616 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 617 | "engines": { 618 | "node": ">=0.6" 619 | } 620 | }, 621 | "node_modules/type-is": { 622 | "version": "1.6.18", 623 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 624 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 625 | "dependencies": { 626 | "media-typer": "0.3.0", 627 | "mime-types": "~2.1.24" 628 | }, 629 | "engines": { 630 | "node": ">= 0.6" 631 | } 632 | }, 633 | "node_modules/unpipe": { 634 | "version": "1.0.0", 635 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 636 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 637 | "engines": { 638 | "node": ">= 0.8" 639 | } 640 | }, 641 | "node_modules/utils-merge": { 642 | "version": "1.0.1", 643 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 644 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", 645 | "engines": { 646 | "node": ">= 0.4.0" 647 | } 648 | }, 649 | "node_modules/vary": { 650 | "version": "1.1.2", 651 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 652 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", 653 | "engines": { 654 | "node": ">= 0.8" 655 | } 656 | } 657 | } 658 | } 659 | -------------------------------------------------------------------------------- /5.express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5.express", 3 | "version": "1.0.0", 4 | "description": "my-express-app", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.18.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /6.express/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | node_modules 4 | README.txt 5 | -------------------------------------------------------------------------------- /6.express/.env.template: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | APP_COLOR=blue 3 | NODE_ENV=development 4 | -------------------------------------------------------------------------------- /6.express/Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM node:alpine 2 | # FROM node:18-slim 3 | FROM node:20-slim 4 | 5 | WORKDIR /app 6 | 7 | COPY . . 8 | 9 | # RUN npm install --production 10 | RUN npm install --omit=dev 11 | 12 | CMD ["node", "app.js"] 13 | -------------------------------------------------------------------------------- /6.express/app.js: -------------------------------------------------------------------------------- 1 | // linux/mac: export APP_COLOR=blue 2 | // windows: set APP_COLOR=green 3 | 4 | const express = require('express'); 5 | const nunjucks = require('nunjucks'); 6 | const morgan = require('morgan'); 7 | require('dotenv').config(); 8 | 9 | const app = express(); 10 | const port = process.env.PORT || 3000; 11 | const color = process.env.APP_COLOR; 12 | const logFormat = process.env.NODE_ENV === 'production' ? 'combined' : 'dev'; 13 | 14 | nunjucks.configure('views', { 15 | autoescape: true, 16 | express: app, 17 | }); 18 | 19 | // View Engine 설정 20 | app.set('view engine', 'html'); 21 | 22 | // Morgan 설정 23 | morgan.token('remote-addr', (req) => { 24 | // x-forwarded-for 헤더가 있는 경우 그 값을 사용하고, 없으면 기본 값인 req.ip를 사용 25 | return req.headers['x-forwarded-for'] || req.ip || '-'; 26 | }); 27 | 28 | // app.use(morgan('combined')); 29 | app.use(morgan(logFormat)); 30 | 31 | app.get('/', (req, res) => { 32 | res.render('hello.html', { color }); 33 | }); 34 | 35 | // /crash 라우트 - 고의적으로 서버 크래시 발생 36 | app.get('/crash', (req, res) => { 37 | console.log('Server is about to crash...'); 38 | throw new Error('Intentional Server Crash'); // 서버 강제 종료 39 | // process.exit(1); // 프로세스를 완전히 종료 (비정상 종료 코드 1) 40 | }); 41 | 42 | app.listen(port, () => { 43 | console.log(`Server is running on http://localhost:${port}`); 44 | }); 45 | -------------------------------------------------------------------------------- /6.express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "6.express", 3 | "version": "1.0.0", 4 | "description": "my-express-app", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "dotenv": "^16.3.1", 14 | "express": "^4.18.2", 15 | "morgan": "^1.10.0", 16 | "nunjucks": "^3.2.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /6.express/views/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello Express 7 | 8 | 9 |
10 |

Hello, World!

11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /7.java/Dockerfile: -------------------------------------------------------------------------------- 1 | # 컨테이너 환경에서 컴파일 예시 2 | FROM openjdk 3 | 4 | ADD HelloWorld.java . 5 | 6 | RUN javac HelloWorld.java 7 | 8 | ENTRYPOINT ["java", "HelloWorld"] 9 | -------------------------------------------------------------------------------- /7.java/Dockerfile.v2: -------------------------------------------------------------------------------- 1 | # 컨테이너 환경에서 바이너리(class) 실행 예시 2 | FROM alpine 3 | 4 | ADD HelloWorld.class HelloWorld.class 5 | 6 | RUN apk --update add openjdk8-jre 7 | 8 | ENTRYPOINT ["java", "HelloWorld"] 9 | -------------------------------------------------------------------------------- /7.java/Dockerfile.v3: -------------------------------------------------------------------------------- 1 | # 멀티 스테이지 빌드를 통해 올바르게 컴파일과 실행하는 예시 2 | # Build Stage (임시환경) 3 | FROM openjdk:8-alpine AS builder 4 | 5 | ADD HelloWorld.java HelloWorld.java 6 | 7 | RUN javac HelloWorld.java 8 | 9 | # Package Stage (운영환경) 10 | FROM openjdk:8-jre-alpine 11 | 12 | COPY --from=builder HelloWorld.class . 13 | 14 | ENTRYPOINT ["java", "HelloWorld"] 15 | -------------------------------------------------------------------------------- /7.java/Dockerfile.v4: -------------------------------------------------------------------------------- 1 | # First stage: complete build environment 2 | FROM maven:3.5.0-jdk-8-alpine AS builder 3 | 4 | # add pom.xml and source code 5 | ADD ./pom.xml pom.xml 6 | ADD ./src src/ 7 | 8 | # package jar 9 | RUN mvn clean package 10 | 11 | # Second stage: minimal runtime environment 12 | FROM openjdk:8-jre-alpine 13 | 14 | # copy jar from the first stage 15 | COPY --from=builder target/my-app-1.0-SNAPSHOT.jar my-app-1.0-SNAPSHOT.jar 16 | 17 | EXPOSE 8080 18 | 19 | CMD ["java", "-jar", "my-app-1.0-SNAPSHOT.jar"] 20 | -------------------------------------------------------------------------------- /7.java/HelloWorld.java: -------------------------------------------------------------------------------- 1 | public class HelloWorld { 2 | public static void main(String[] args) { 3 | System.out.println("hello, world"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /7.java/README.txt: -------------------------------------------------------------------------------- 1 | # 멀티 스테이지 빌드를 통한 올바른 컴파일 2 | # https://docs.docker.com/build/building/multi-stage/ 3 | 4 | 5 | # 멀티 플랫폼을 위한 올바른 이미지 빌드 6 | # https://docs.docker.com/build/building/multi-platform/ 7 | 8 | -------------------------------------------------------------------------------- /8.golang/Dockerfile: -------------------------------------------------------------------------------- 1 | # 컨테이너 환경에서 컴파일 예시 2 | FROM golang:alpine 3 | 4 | ADD . . 5 | 6 | RUN go build main.go 7 | 8 | CMD ["./main"] 9 | -------------------------------------------------------------------------------- /8.golang/Dockerfile.v2: -------------------------------------------------------------------------------- 1 | # 멀티 스테이지 빌드를 통해 올바르게 컴파일과 실행하는 예시 2 | # Build Stage (임시환경) 3 | FROM golang:alpine AS builder 4 | 5 | ADD main.go . 6 | RUN go build main.go 7 | 8 | # Package Stage (운영환경) 9 | FROM alpine 10 | COPY --from=builder /go/main /go/main 11 | 12 | CMD ["/go/main"] 13 | -------------------------------------------------------------------------------- /8.golang/Dockerfile.v3: -------------------------------------------------------------------------------- 1 | # first stage build 2 | FROM golang:alpine AS builder 3 | ENV GO111MODULE=on CGO_ENABLE=0 GOOS=linux GOARCH=amd64 4 | 5 | WORKDIR /build 6 | 7 | COPY go.mod go.sum main.go ./ 8 | RUN go mod download 9 | RUN go build -o main 10 | 11 | WORKDIR /dist 12 | RUN cp /build/main . 13 | 14 | # second stage build 15 | FROM alpine 16 | COPY --from=builder /dist/main . 17 | 18 | ENTRYPOINT ["./main"] 19 | -------------------------------------------------------------------------------- /8.golang/README.txt: -------------------------------------------------------------------------------- 1 | # 멀티 스테이지 빌드를 통한 올바른 컴파일 2 | # https://docs.docker.com/build/building/multi-stage/ 3 | 4 | 5 | # 멀티 플랫폼을 위한 올바른 이미지 빌드 6 | # https://docs.docker.com/build/building/multi-platform/ 7 | 8 | -------------------------------------------------------------------------------- /8.golang/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Printf("hello, world\n") 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker 튜토리얼 2 | ## 도커 개요 3 | - (생략 - 수업 참조) 4 | 5 | ## 도커 명령어 기초 배우기 6 | ### 0.commands 7 | - [README.md 참고](https://github.com/lovehyun/tutorial-docker/tree/master/0.commands) 8 | 9 | ## 도커 이미지 빌드 10 | - 공식 메뉴얼 : ` https://docs.docker.com/engine/reference/commandline/build/ ` 11 | - 이미지 빌드 후 생긴 이미지들 모두 삭제 12 | - ` docker rmi $(docker images --filter "dangling=true" -q --no-trunc) ` 13 | 14 | ## 튜토리얼 코드 설명 15 | - 도커 튜토리얼을 따라히실 분들은 디렉토리 순서대로 학습 하시기 바랍니다. 16 | 17 | ### 1.hello 18 | - 도커의 핼로우 월드 19 | - 빌드 : ` docker build --tag myhello:0.1 . ` 20 | - 실행 : ` docker run --rm myhello:0.1 ` 21 | - 주요 관점 22 | 1. 이미지 빌드하기 23 | 2. Ubuntu 베이스 이미지와 Alpine 베이스 이미지의 용량 차이 비교 24 | 3. 각종 주요 도커파일 키워드(명령어) 확인 25 | 26 | ### 2.nginx 27 | - nginx 내손으로 만들기 28 | - 빌드 : ` docker build --tag mynginx:0.1 . ` 29 | - 실행 : ` docker run -d -p 80:80 mynginx:0.1 ` 30 | - 주요 관점 31 | 1. 리얼 이미지 만들기 32 | 2. non-interactive 한 형태로 도커파일 만들기 33 | 3. daemonize 되지 않는, foreground 형태로 실행파일 만들기 (콘솔 로그까지 출력되면 best) 34 | 4. TZ 및 clean-up 까지 처리 하면 완벽한 마무리 35 | 36 | ### 3.flask 37 | - flask 앱 만들기 38 | - 빌드 : ` docker build --tag myflask:0.1 . ` 39 | - 실행 : ` docker run -p 5000:5000 myflask:0.1 ` 40 | - 주요 관점 41 | 1. 실제 앱 만들기 42 | 2. 파일의 복사 43 | 3. 불필요한 파일의 예외처리(.dockerignore) 44 | 45 | ### 4.flask 46 | - flask 앱 만들기 47 | - 빌드 : ` docker build --tag myflask:0.2 . ` 48 | - 실행 : ` docker run -d -e APP_COLOR=red -p 5000:5000 myflask:0.2 ` 49 | - 주요 관점 50 | 1. 더 리얼한 앱 만들기 51 | 2. 파일 및 디렉토리의 복사 52 | 3. 패키지의 설치 및 Dev 와 Ops 환경의 sync 맞추기 53 | 54 | ### 5.express 55 | - express 앱 만들기 56 | - 빌드 : ` docker build --tag myexpress:0.1 . ` 57 | - 실행 : ` docker run -d -p 8000:8000 myexpress:0.1 ` 58 | - 주요 관점 59 | 1. JS 앱 만들기 60 | 2. npm, package.json, package-lock.json 및 node_modules 의 명확한 이해 61 | 3. 불필요한 개발도구 파일의 운영 환경에서의 제거 62 | - ```bash 63 | npm i express # npm install express 64 | npm i -D nodemon # npm install nodemon --save-dev 65 | 66 | npm install --production # export NODE_ENV=production 및 npm install 67 | script { "start": "nodemon app.js" } 68 | npm start 69 | ``` 70 | 4. Dev 와 Ops 환경의 sync 맞추기 71 | 72 | ### 6.java 73 | - java 앱 만들기 74 | - 빌드 : 75 | ```bash 76 | docker build --tag javaapp:0.1 . 77 | docker build --tag javaapp:0.2 -f Dockerfile.v2 . 78 | docker build --tag javaapp:0.3 -f Dockerfile.v3 . 79 | ``` 80 | - 실행 : ` docker run --rm javaapp:0.1 ` 81 | - 주요 관점 82 | 1. 싱글 스테이지 빌드 (비효율적 컴파일) 83 | 2. 싱글 스테이지 빌드 (잘못된 바이너리 복사) 84 | 3. 멀티 스테이지 빌드 (올바른 컴파일 및 이미지 최적화) 85 | 4. 멀티 스테이지 빌드 (BEST 예시) 86 | 87 | ### 7.golang 88 | - golang 앱 만들기 89 | - 빌드 : 90 | ```bash 91 | docker build --tag goapp:0.1 . 92 | docker build --tag goapp:0.2 -f Dockerfile.v2 . 93 | ``` 94 | - 주요 관점 95 | 1. 싱글 스테이지 빌드 (비효율적 컴파일) 96 | 2. 멀티 스테이지 빌드 (올바른 컴파일 및 이미지 최적화) 97 | 3. 멀티 스테이지 빌드 (BEST 예시) 98 | 99 | 100 | ### 9.dockercompose 101 | - dockercompose 를 사용한 개발/배포/운영 102 | 103 | 104 | ## 도커허브 105 | - https://hub.docker.com/ 106 | 107 | ### 이미지 푸시 108 | - 계정 로그인 : ` docker login ` 109 | - 이미지 태깅 : ` docker tag ` 110 | - 이미지 푸시 : ` docker push ` 111 | --------------------------------------------------------------------------------