Background

最近在評估新服務要不要使用K8S,所以想說先來把整個Docker複習一次,畢竟之前只有小玩一下,要正式應用在產品上算是第一次。以下就簡單記錄一下一些Dockerk的基本應用。本章的是以這篇文章 提供的程式碼當作範例,詳細的程式碼介紹可以到上面的文章內詳讀。

Start

因為我自己本身比較熟 Python + Flask 所以 build image 的部分只會先以 server 的部分來做範例。整個project之後會再寫一篇 Docker compose 的文章來介紹。

$ cd flask-vue-crud/server
$ tree
.
├── Dockerfile
├── __pycache__
│   └── run.cpython-37.pyc
├── requirements.txt
└── run.py

run.py

from flask import Flask, jsonify, request
from flask_cors import CORS
import uuid

BOOKS = [
    {
        'id': uuid.uuid4().hex,
        'title': 'On the Road',
        'author': 'Jack Kerouac',
        'read': True
    },
    {
        'id': uuid.uuid4().hex,
        'title': 'Harry Potter and the Philosopher\'s Stone',
        'author': 'J. K. Rowling',
        'read': False
    },
    {
        'id': uuid.uuid4().hex,
        'title': 'Green Eggs and Ham',
        'author': 'Dr. Seuss',
        'read': True
    }
]

# configuration
DEBUG = True

# instantiate the app
app = Flask(__name__)
app.config.from_object(__name__)

# enable CORS
CORS(app, resources={r'/*': {'origins': '*'}})


# sanity check route
@app.route('/ping', methods=['GET'])
def ping_pong():
    return jsonify('pong!')


@app.route('/books', methods=['GET', 'POST'])
def all_books():
    response_object = {'status': 'success'}
    if request.method == 'POST':
        post_data = request.get_json()
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book added!'
    else:
        response_object['books'] = BOOKS
    return jsonify(response_object)


@app.route('/books/<book_id>', methods=['PUT', 'DELETE'])
def single_book(book_id):
    response_object = {'status': 'success'}
    if request.method == 'PUT':
        post_data = request.get_json()
        remove_book(book_id)
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book updated!'
    if request.method == 'DELETE':
        remove_book(book_id)
        response_object['message'] = 'Book removed!'
    return jsonify(response_object)

def remove_book(book_id):
    for book in BOOKS:
        if book['id'] == book_id:
            BOOKS.remove(book)
            return True
    return False

if __name__ == '__main__':
    app.run()

requirements.txt

Click==7.0
Flask==1.0.2
Flask-Cors==3.0.7
gevent==1.4.0
greenlet==0.4.15
gunicorn==19.9.0
itsdangerous==1.1.0
Jinja2==2.10.1
MarkupSafe==1.1.1
six==1.12.0
Werkzeug==0.15.4

Dockerfile

FROM python:3.7.3-stretch

# set working directory
WORKDIR '/app'

COPY ./requirements.txt ./
RUN pip install -r requirements.txt

COPY . .

# Expose the port uWSGI will listen on
EXPOSE 8000

CMD ["gunicorn", "-b", "0.0.0.0:8000", "run:app"]

Explain

只針對 Dockerfile 做講解,那整個 Dockerfile 主要架構我自己覺得有三個部分,基本上把這三個部分填完剩下在做稍微的補充整個 Dockerfile 就很完整了。

  1. 拿哪一個 docker image 當做基底, 對應到 Dockerfile 就是 FROM python:3.7.3-stretch 那每個語言基本上都有他自己的基底,大家可以上 Docker Hub 去尋找。

  2. 下載&安裝相關的package,那對應到的指令是

COPY ./requirements.txt ./
RUN pip install -r requirements.txt

基本上每個語言也都有自己的 package manage tool 和安裝方法所以邏輯上這段也是不可或缺的。

  1. 接著就是如何請動你的container service,對應到的指令 CMD ["gunicorn", "-b", "0.0.0.0:8000", "run:app"]

  2. 最後就是補充上面三個主要架構所沒提到的地方。

# set working directory
# 指定程式放置的位置
WORKDIR '/app'

# copy current folder into image
# 複製程式到指定位置
COPY . .

# Expose the port uWSGI will listen on
# 對外監聽的 port (不是 localhost, 是 container)
EXPOSE 8000

Build & Start

$ docker build -t taikerliang/flask-vue-crud-server .

Sending build context to Docker daemon  35.91MB
Step 1/7 : FROM python:3.7.3-stretch
 ---> 34a518642c76
Step 2/7 : WORKDIR '/app'
 ---> Using cache
 ---> adf7300a96dd
Step 3/7 : COPY ./requirements.txt ./
 ---> Using cache
 ---> 31aeca5fc2d8
Step 4/7 : RUN pip install -r requirements.txt
 ---> Using cache
 ---> 1ea6f0b0af38
Step 5/7 : COPY . .
 ---> e84c9c799ca0
Step 6/7 : EXPOSE 8000
 ---> Running in c0e41407e8cb
Removing intermediate container c0e41407e8cb
 ---> 7005ed4da085
Step 7/7 : CMD ["gunicorn", "-b", "0.0.0.0:8000", "run:app"]
 ---> Running in 0a75b6785be4
Removing intermediate container 0a75b6785be4
 ---> 0cb8c20ec322
Successfully built 0cb8c20ec322
Successfully tagged taikerliang/flask-vue-crud-server:latest

建立好之後我們透過 docker images 來看一下

$ docker images
REPOSITORY                          TAG                 IMAGE ID            CREATED             SIZE
taikerliang/flask-vue-crud-server   latest              0cb8c20ec322        
taikerliang/flask-vue-crud-client   latest              5fc0d4a6664f        13 hours ago        386MB
python                              3.7.3-stretch       34a518642c76        3 weeks ago         929MB
node                                lts-alpine          9dfa73010b19        4 weeks ago         75.3MB

有看到 taikerliang/flask-vue-crud-server 這個 image 即表示成功。接下來我們來run看看我們剛剛build好的image。

# -p => redirect localhost 5000 requests to conatiner 8000 port
$ docker run -p 5000:8000 0cb8c20ec322
[2019-07-05 04:39:40 +0000] [1] [INFO] Starting gunicorn 19.9.0
[2019-07-05 04:39:40 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1)
[2019-07-05 04:39:40 +0000] [1] [INFO] Using worker: sync
[2019-07-05 04:39:40 +0000] [9] [INFO] Booting worker with pid: 9

測試一下 我們打開瀏覽器 輸入 http://localhost:5000/books 會看到以下內容就表示一切都沒問題

{
  "books": [
    {
      "author": "Jack Kerouac", 
      "id": "320bdb5c6e3742bc9f1906e8c9fc705b", 
      "read": true, 
      "title": "On the Road"
    }, 
    {
      "author": "J. K. Rowling", 
      "id": "f8dde7d366884d7cb280919568cbffec", 
      "read": false, 
      "title": "Harry Potter and the Philosopher's Stone"
    }, 
    {
      "author": "Dr. Seuss", 
      "id": "4902ab6870334b2f90ff8c7c08dcc5e6", 
      "read": true, 
      "title": "Green Eggs and Ham"
    }
  ], 
  "status": "success"
}

Push images to docker hub & AWS ECR

建好了image我們會希望把他push到雲端上去這樣我們才可以很方便地在任何地方使用這個image。Docker Hub, AWS, GCP 等都有提供類似的服務讓你上傳你的image。 在這邊我們介紹 Docker Hub & AWS ECR。

那上傳到Docker Hub其實非常的簡單,有一組Docker的帳號密碼即可。輸入以下的指令,並指定你要上傳的 image (記得要先login,docker login 應該可以幫助你)

$ docker push taikerliang/flask-vue-crud-server:latest
The push refers to repository [docker.io/taikerliang/flask-vue-crud-server]
67d42bab0d2f: Pushed
6a65a9f4895d: Pushed
a6456fa3f5e8: Pushed
a4063a0e21f6: Pushed
799a7872c8c7: Mounted from library/python
715450468940: Mounted from library/python
c9d608035aef: Mounted from library/python
bb9c02680a15: Mounted from library/python
a637c551a0da: Mounted from library/python
2c8d31157b81: Mounted from library/python
7b76d801397d: Mounted from library/python
f32868cde90b: Mounted from library/python
0db06dff9d9a: Mounted from library/python
latest: digest: sha256:fbdc22d7752eccf4d9ea86bac1cc74b74140b62f41f6f37b139b4c2d6b7311d0 size: 3054

那我們打開 docker hub 的網頁就會看到一個新的 image:

Screen-Shot-2019-07-05-at-1.37.51-PM

接著下來是傳到 AWS ECR,這邊會用到 AWC CLI Toll (我這邊就不介紹如何安裝)。

get login info

$ aws ecr get-login --no-include-email

輸入完這個指令之後會跳出一個 docker login 的指令,直接複製然後輸入(這個會取代你原本登入docker hub的資訊),偷懶的人可以直接下 eval $(aws ecr get-login --no-include-email)

tag image

那在 push 到 AWS ecr 之前我們要先對我們準備要 push 的image做一些 AWS 專屬的 tag 指令如下:

docker tag 0cb8c20ec322 <aws_account_id>.dkr.ecr.us-east-1.amazonaws.com/<new_repository_name>:<tag_name>

push image

docker push <aws_account_id>.dkr.ecr.us-east-1.amazonaws.com/<new_repository_name>:<tag_name>
  • P.S aws_account_id 在 AWS my accout console 中可以找到(是一串數字)

ok 那整個 docker 的基本介紹就到這邊。

Reference