顯示具有 Linux 標籤的文章。 顯示所有文章
顯示具有 Linux 標籤的文章。 顯示所有文章

2025/05/02

Log Handle In Docker

Docker 現在已經是可以當 production server 用的工具了,來聊一下他對 logs 有哪些配置方式。

根據 docker 官方文件,目前有以下幾個配置方法:

Driver 說明
none 容器沒有可用的日誌,且 docker logs 不會返回任何輸出。
local 日誌以一種為了最小化開銷而設計的自定義格式儲存。
json-file 日誌格式為 JSON。Docker 的預設日誌驅動程式。
syslog 將日誌消息寫入 syslog 設施。主機上必須運行 syslog 守護程序。
journald 將日誌消息寫入 journald。主機上必須運行 journald 守護程序。
gelf 將日誌消息寫入 Graylog 擴展日誌格式 (GELF) 端點,如 Graylog 或 Logstash。
fluentd 將日誌消息寫入 fluentd(轉發輸入)。主機上必須運行 fluentd 守護程序。
awslogs 將日誌消息寫入 Amazon CloudWatch Logs。
splunk 使用 HTTP 事件收集器將日誌消息寫入 Splunk。
etwlogs 將日誌消息寫入 Windows 事件追蹤 (ETW) 事件。僅適用於 Windows 平台。
gcplogs 將日誌消息寫入 Google Cloud Platform (GCP) Logging。

如果你能擁有自己的 log server,會建議把 log 打進 log api server,擁有權限的人就可以在 web tool 上做所有的查詢以及決定資料怎麼保留跟處理,畢竟如果 log 停留在各 server,以現在很多服務走 HA 或是 k8s 情況下,遇到狀況你到底要進哪台服務查詢都會成為問題。

不過現實生活中很多人都只有一台 server,而且也沒有 api log server 這種資源,只是要有 log 即可的話,我們一般會用 json-file 這個選項,這也是 docker 預設的 log driver。

docker-compose json-file 範例

services:
    nginx:
        image: nginx
        container_name: nginx
        ports:
            - "80:80"
        volumes:
            - ./nginx.conf:/etc/nginx/nginx.conf
        logging:
            driver: json-file
            options:
                max-size: "10m"
                max-file: "3"

這個範例會把 nginx 的 log 儲存到 /var/lib/docker/containers/<container-id>/<container-id>-json.log,當 log 檔案大小超過 10MB 時,docker 會自動將舊的 log 檔案重新命名為 <container-id>-json.log.1,並開始寫入新的 log 檔案,最多會保留 3 個 log 檔案,當你使用指令 docker logs 時,docker 會自動讀取最新的 log 檔案,並顯示在終端機上。

一般 image 如 nginx,他會把 log 設定成 stdout,這樣 docker 才能夠讀取到 log,然後寫入到你設定的 log driver 裡面,選擇 json-file 時 docker 會自動幫你把 stdout 或 stderr 的 log 轉成 json 格式,然後寫入到你設定的 log driver 裡面。

nginx 的話,他的配置會是這樣:

access_log /dev/stdout;
error_log /dev/stderr;

這樣的缺點是,access log 會跟 error log 都寫在同一個檔案裡面,會造成你在查詢 log 的時候不方便,假設我們想要使用 docker,又想要產生跟傳統 web server 一樣的 log,我們可以修改 nginx.conf,讓他把 log 寫到 /var/log/nginx/access.log/var/log/nginx/error.log

Nginx Config 範例

nginx.conf

http {
    server {
        listen       80;
        server_name  localhost;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;
    }
}

然後把 log 目錄掛載出來。

docker-compose.yml

services:
    nginx:
        image: nginx
        container_name: nginx
        ports:
            - "80:80"
        volumes:
            - ./nginx.conf:/etc/nginx/nginx.conf
            - ./logs:/var/log/nginx

我們在 /etc/logrotate.d/nginx 新增一個檔案,然後把以下的內容貼上去:

logrotate conf

/var/log/nginx/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec nginx nginx -s reopen
    endlaction
}

docker exec nginx nginx -s reopen 的用意是 logrotate 在 rotate 之後需要呼叫 docker 重新開啟 nginx 的 log,不這樣的話 nginx in docker 會使用你 rename 後的 log 檔案繼續寫 log。

當我們使用 compress,並且會針對壓縮過後的檔案做後續處理的話,一定要有 sharedscripts 以及使用 lastaction,如果使用 postrotate 的話,他不會等所有的檔案都執行完壓縮就直接作動了。

這邊有幾個 logrotate 的指令可以使用:

指令 說明
prerotate/endscript 在進行日誌輪轉之前執行的指令,每個匹配的日誌檔案都會執行一次
postrotate/endscript 在進行日誌輪轉之後執行的指令,每個匹配的日誌檔案都會執行一次
firstaction/endscript 在所有匹配日誌檔案進行輪轉之前執行的指令,只執行一次
lastaction/endscript 在所有匹配日誌檔案進行輪轉之後執行的指令,只執行一次
preremove/endscript 在舊的日誌檔案被移除之前執行的指令

另外有一種不用重新開啟檔案的配置參數 copytruncate,這個參數會在 logrotate 開始 rotate 的時候複製原本的 log 檔案,然後 truncate 原本的 log 檔案,這樣 nginx 就不需要重新開啟檔案了,但這樣有機會漏寫紀錄,所以不建議使用這個參數。

Apache2 Config 範例

apache2 的原始 log 配置會是:

ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 common

我們要修改成:

ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined

然後把 log 目錄掛載出來。

docker-compose.yml

services:
    apache2:
        image: httpd
        container_name: httpd
        ports:
            - "80:80"
        volumes:
            - ./httpd.conf:/usr/local/apache2/conf/httpd.conf
            - ./logs:/var/log/apache2

httpd.conf

<VirtualHost *:80>
    ServerName localhost
    DocumentRoot /var/www/html

    ErrorLog /var/log/apache2/error.log
    CustomLog /var/log/apache2/access.log combined
</VirtualHost>

logrotate conf

/var/log/apache2/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec apache2 apachectl -k graceful
    endscript
}

這個範例跟 nginx 的範例一樣,只是把 nginx -s reopen 改成 apachectl -k graceful

假設你 logrotate 後要執行其他 bash 做其他處理的話。

/var/log/apache2/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec httpd apachectl -k graceful
        /usr/bin/bash /usr/local/bin/other_script.sh
    endscript
}

2025/04/02

Neovim Installation

neovim 是我即便是在 windows 下也會開來使用的 command line editor,來 memo 一下 Windows 跟 Linux 怎麼安裝。

Windows

我在 Windows 上都是使用 scoop 來管理套件,所以 neovim 也是用 scoop 來安裝,這樣好處是我用 scoop update 時都可以保持拿到最新的 neovim

scoop install neovim

powershell 可以執行指令安裝 vim-plug

iwr -useb https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim | ni $HOME\AppData\Local\nvim\autoload\plug.vim -Force

接下來就是配置你的個人檔案,位置在 C:\Users\你的使用者名稱\AppData\Local\nvim\init.vim,可以使用任何編輯軟體包括 neovim 開啟他,把 config 都配好以後,執行 neovim,首次執行會有很多錯誤是正常的,因為你的 plugin 並沒有真的安裝,開啟後執行 :PluginInstall,關閉再開啟 neovim 應該就正常了。

Linux

Linux 的部分有兩種方法,一個是透過你自己的 OS 去找最新的 repo 安裝,另一個是可以去 https://github.com/neovim/neovim/releases/,neovim 的 github 官網找最新版本的 appimage 檔案,只要你的 OS 有 FUSE 就可以直接使用,以 ubuntu 22 為例,此文章撰寫時最新的穩定版本是 NVIM v0.11.0

curl -LO https://github.com/neovim/neovim/releases/download/v0.11.0/nvim-linux-x86_64.appimage
chmod +x nvim-linux-x86_64.appimage
sudo mv nvim-linux-x86_64.appimage /usr/local/bin/nvim

執行完這段就有 nvim 可以執行了,接著裝套件。

sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs \
       https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'

mkdir -p ~/.config/nvim
touch ~/.config/nvim/init.vim
nvim ~/.config/nvim/init.vim

接著就跟 Windows 一樣安裝套件後就可以用了,之後要更新 neovim 版本只要抓最新版本的 appimage 替換即可。

2025/02/12

Linux Find

之前客戶 server 遇到單一目錄檔案過大問題,使用 rm -f * 或者是砍掉整個目錄都遇到 arguments 過多問題,最終使用 find 指令解決,有在使用 Linux 的人一定知道 find 的強大,在這邊作把常用的案例濃縮筆記一下。

範例

一般

find . -type f -name "lebron*" # 搜尋 lebron 為首的檔案
find . -type f -iname "lebron*" # 搜尋 lebron 為首的檔案(不區分大小寫)
find . -type d -name "lebron*" # 搜尋 lebron 為首的目錄

時間類

參數 說明 單位 範例 說明
atime 檔案最後存取的時間 find /path -atime -7 找出 7 天內被存取過的檔案
ctime 檔案的 inode 最後變更時間 find /path -ctime +30 找出 30 天前 inode 變更的檔案(如權限修改)
mtime 檔案內容的最後修改時間 find /path -mtime 0 找出今天內修改過內容的檔案
amin 檔案最後存取的時間 分鐘 find /path -amin -60 找出 60 分鐘內被存取過的檔案
cmin 檔案的 inode 最後變更時間 分鐘 find /path -cmin +120 找出 120 分鐘前 inode 變更的檔案
mmin 檔案內容的最後修改時間 分鐘 find /path -mmin -30 找出 30 分鐘內修改過內容的檔案

-time

指令 查詢範圍 檔案舉例(假設今天是 2024/02/21)
-mtime +3 4 天前或更早 ✅ 2024/02/17、✅ 2024/02/16
-mtime 3 剛好 3 天前 2024/02/18
-mtime -3 最近 3 天內(包含今天) ✅ 2024/02/21、✅ 2024/02/20、✅ 2024/02/19
-mtime 0 今天(00:00 之後) 2024/02/21(今天)

min

指令 查詢範圍 檔案舉例(假設現在是 12:00 PM)
-mmin +30 31 分鐘前或更早 ✅ 11:29 AM、✅ 11:00 AM
-mmin 30 剛好 30 分鐘前 11:30 AM
-mmin -30 最近 30 分鐘內(含現在) ✅ 12:00 PM、✅ 11:50 AM、✅ 11:45 AM
-mmin 0 最近 1 分鐘內(剛改變) 12:00 PM

空間類

參數 說明 單位 範例 說明
-size 指定檔案大小 512-byte blocks (預設) find /path -size +100M 找出大於 100MB 的檔案
-empty 找出空檔案或空目錄 N/A find /path -empty 找出空檔案或空目錄
-maxdepth 限制搜尋深度 N/A find /path -maxdepth 2 -size +1G 限制搜尋到最多 2 層目錄內,找出大於 1GB 的檔案
-type f 限制搜尋結果為檔案 N/A find /path -type f -size -50k 找出小於 50KB 的檔案
-type d 限制搜尋結果為目錄 N/A find /path -type d -size +10M 找出超過 10MB 的目錄(通常為不常見情況)

-size 常見用法:

  • find /path -size +1G → 找出 大於 1GB 的檔案
  • find /path -size -500k → 找出 小於 500KB 的檔案
  • find /path -size 100M → 找出 剛好 100MB 的檔案
  • find /path -size +100M -size -500M找出大小介於 100MB ~ 500MB 之間的檔案

-size 可用單位:

  • cbytes(例如 -size +1024c 找出大於 1024 bytes 的檔案)
  • kKB(例如 -size -500k 找出小於 500KB 的檔案)
  • MMB(例如 -size +10M 找出大於 10MB 的檔案)
  • GGB(例如 -size +1G 找出大於 1GB 的檔案)

顯示

格式 說明 範例
%p 檔案的完整路徑 ./test.txt
%f 檔案的名稱(不包括路徑) test.txt
%s 檔案的大小(以 byte 為單位) 1024
%M 檔案的權限(如 ls -l 顯示) -rw-r--r--
%u 檔案的擁有者 user1
%g 檔案的群組 group1
%T@ 檔案的最後修改時間,Unix 時間戳 1592390505.000000000
%TY-%Tm-%Td 檔案的修改日期(年-月-日) 2024-02-12
%TH:%TM:%TS 檔案的修改時間(時:分:秒) 14:30:15
%p 檔案的路徑 ./folder/test.txt
%k 檔案的大小(以 1KB 為單位,四捨五入) 1
%l 檔案是符號連結時,顯示其指向的檔案路徑 ./link_to_file.txt
%c 檔案的創建時間(僅支援某些檔案系統) 2024-02-10 12:00:00

顯示檔案名稱與大小(以 KB 為單位)

find . -type f -printf "%s KB\t%p\n"
1024 KB    ./file1.txt
2048 KB    ./file2.log

顯示檔案的修改日期與名稱

find . -type f -printf "%TY-%Tm-%Td %p\n"
2024-02-12 ./file1.txt
2024-02-12 ./file2.log

顯示檔案權限、擁有者、群組、大小和名稱

find . -type f -printf "%M %u %g %8s %p\n"
-rw-r--r-- user1 group1    1024 ./file1.txt
-rw-r--r-- user2 group2    2048 ./file2.txt

執行類

-printf

指令 說明 範例 解釋
-exec 對符合條件的檔案執行指令 find /path -type f -name "*.log" -exec rm {} \; 刪除所有 .log 檔案
-exec (多個參數) 傳遞多個參數到指令 find /path -type f -name "*.txt" -exec mv {} /backup/ \; 移動 .txt 檔案到 /backup/
-exec (多檔案批次處理) + 來一次傳遞多個檔案 find /path -type f -name "*.jpg" -exec ls -lh {} + 列出 .jpg 檔案詳細資訊(批次傳遞參數)
-execdir 在檔案所在目錄執行指令 find /path -type f -name "*.bak" -execdir rm {} \; 在檔案所在目錄刪除 .bak 檔案

刪除所有 .log 檔案:

find /var/log -type f -name "*.log" -exec rm {} \;

批次刪除 .tmp 檔案(避免多次執行 rm):

find /tmp -type f -name "*.tmp" -exec rm {} +

找出 .sh 檔案並修改權限:

find /scripts -type f -name "*.sh" -exec chmod +x {} \;

計算 .log 檔案總大小:

find /var/log -type f -name "*.log" -exec du -ch {} + | tail -n 1

-delete

指令 說明 範例 解釋
-delete 刪除符合條件的檔案 find /path -type f -name "*.log" -delete 刪除所有 .log 檔案
-delete (空目錄) 刪除空目錄 find /path -type d -empty -delete 刪除所有空目錄
-delete (限制層級) 限制搜尋深度刪除 find /path -maxdepth 1 -type f -name "*.tmp" -delete 只刪除 /path 下的 .tmp 檔案,不影響子目錄

其他

參數 說明 範例 解釋
-user 搜尋指定使用者擁有的檔案 find /path -user username 找出屬於指定使用者的檔案
-group 搜尋指定群組擁有的檔案 find /path -group groupname 找出屬於指定群組的檔案
-perm 根據檔案權限搜尋檔案 find /path -perm 755 找出擁有 755 權限的檔案
-newer 搜尋修改時間在指定檔案之後的檔案 find /path -newer file1.txt 找出在 file1.txt 之後被修改過的檔案
! -newer 搜尋修改時間在指定檔案之前的檔案 find /path ! -newer file1.txt 找出在 file1.txt 之前被修改過的檔案
-maxdepth 限制搜尋的最大目錄層級 find /path -maxdepth 2 只搜尋最多 2 層目錄深度的檔案
-mindepth 限制搜尋的最小目錄層級 find /path -mindepth 2 只搜尋從第 2 層開始的檔案
-print0 輸出檔案名稱並以 null 字元結尾,用於處理空格 find /path -print0 方便與 xargs -0 搭配使用,避免檔名有空格
-0 接受 null 字元分隔的檔案名稱 find /path -print0 | xargs -0 rm 用於 xargs,確保檔名包含空格的情況下也能正確處理
-iname 不區分大小寫的檔案名稱搜尋 find /path -iname "*.jpg" 搜尋所有 .jpg 檔案,不區分大小寫
-empty 搜尋空的檔案或目錄 find /path -empty 找出所有空的檔案或目錄
-exec 執行指定的命令來處理找到的檔案 find /path -name "*.log" -exec ls -lh {} \; 對每個找到的檔案執行 ls -lh
-exec + 批次處理所有符合條件的檔案,提高效率 find /path -name "*.log" -exec ls -lh {} + 批次執行命令,將多個檔案一次傳遞給命令處理
-prune 排除特定目錄,使其不被搜尋 find /path -path "/path/exclude" -prune -o -name "*.txt" -print 排除 /path/exclude 目錄,只搜尋其他 .txt 檔案
-follow 跟隨符號連結 find /path -follow -name "*.txt" 跟隨符號連結,搜尋 .txt 檔案

2024/07/11

Create Local SSL For Development

內部服務使用 SSL 相關解法

🟣 建立自簽憑證

  • domain name: example.test
  • ip: 10.10.10.22

🎈 此方法不需要擁有真實 domain,但會需要去每個 user 電腦配置內容。

💬 建立 ca 憑證

openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.pem

建立流程 Common Name 最為重要,不可以填錯。

💬 建立 service 憑證需求檔案

openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr

💬 產出 server 憑證

先建立一個文件 server.ext

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = example.test
DNS.2 = *.example.test

💬 產生服務 crt

openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.crt -days 3650 -sha256 -extfile server.ext

💬 配置 nginx.conf

server {
    listen 443 ssl;
    root /var/www/html;
    server_name: example.test;
    index index.html;
    ssl_certificate /etc/ssl/server.crt;
    ssl_certificate_key /etc/ssl/server.key;
}

💬 將 CA 憑證匯入 Windows

Windows Key + r 叫出執行視窗,輸入 certmgr.msc

  1. 選擇受信任的根憑證授權單位
  2. 按下滑鼠右鍵選擇 所有工作 > 匯入
  3. 選擇下一步。
  4. 將檔案類型選擇所有檔案,並且選擇 ca.pem,接著一路下一步以及同意到結束。

💬 修改 user hosts

Windows 的路徑在 C:\Windows\System32\drivers\etc\hosts

填入 10.10.10.22 example.test

這時候在網址打 https://example.test 應可以順利使用 https 連線 server 並且不予以警告。

🟣 其他方法

💬 使用真實憑證

ACME 驗證方式

letsencrypt 可以使用不直連服務驗證的方式發放憑證,可參考文件進行 DNS 認證,但這個方法還是必須有真實 domain name。

💬 Reverse proxy server

使用一台具有 domain public 可用的 reverse server,將他配置好 SSL 資源後將 domain 轉到區網 service,並且限制可連線的區網 IP。

🟣 參考網站

2024/03/25

Delete None Keeping Files Part II

之前寫過這篇 Delete None Keeping Files,在某個目錄依照建日保留最新的 N 個檔案,有個新改良的寫法或許更好懂。

#!/bin/bash

KEEP=10
DIRECTORY=/data/

find "$DIRECTORY" -maxdepth 1 -type f -name "*.log" -printf "%T@ %p\n" | sort -r | tail -n +$(($KEEP+1)) | awk '{print $NF}' | xargs rm -f

來解釋各段參數:

  • find "$DIRECTORY":尋找目標目錄
  • -maxdepth 1:只找該目錄第一層
  • -type f:只找尋檔案,不列入目錄
  • -name '*.log':找尋副檔名為 log 的檔案
  • -printf "%T@ %p\n":使用 printf 輸出,輸出方式為建立的時間戳 + 檔名
  • sort -r:依照時間戳逆排序
  • tail -n +$(($KEEP+1)):列出你要保留的檔案數以外的檔案
  • awk '{print $NF}':只抓出檔案路徑
  • xargs rm -f:刪除該檔案

這樣的寫法應該是萬無一失了。

2022/12/05

From Docker Virtual Network Connect To Solid MySQL Server

假設我們的 server 只有一台,PHP 環境為 docker container,而資料庫為實體機安裝,資料庫 3306 防火牆防堵不對外,但要允許從 Docker 環境內的服務存取,最簡單的方式就是防火牆讓 Docker 產生的 network interface 通過,下面我們來實做這件事。

今天示範環境為使用 Vagrant 起一台 Ubuntu jammy64,然後在裡面進行防火牆的測試。

sudo service ufw start
sudo ufw enable
sudo ufw allow ssh

記得開啟防火牆要先把 ssh 打開,不然就悲劇了,接著安裝 MySQL,docker 以及 docker compose 部分就麻煩自行安裝。

sudo apt install -y mariadb-server mariadb-client

安裝完畢後,我們來建立測試用資料庫以及帳號。

sudo mysql
create database laravel;
GRANT ALL PRIVILEGES ON `laravel`.* TO 'homestead'@'%' IDENTIFIED BY 'secret' WITH GRANT OPTION;

這邊權限開 % 沒關係,因為我們只會允許 docker network 指定的 network interface 進入而已。

docker-compose.yml
version: "3.9"

services:
  fpm:
    container_name: fpm
    build: .
    restart: always
    working_dir: /www
    extra_hosts:
      - "host.docker.internal:host-gateway"
    networks:
      - front_end
    volumes:
      - ./www/:/www

networks:
  front_end:
    driver_opts:
      com.docker.network.bridge.name: br-fpm
Dockerfile
FROM php:8.1-fpm

RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip

ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/

RUN chmod +x /usr/local/bin/install-php-extensions && \
    install-php-extensions @composer pdo_mysql mbstring exif pcntl bcmath gd

WORKDIR /www

這是要安裝 Laravel 的最小環境,我們今天只是要測試連線所以其他的設定就先不理會了。

sudo docker compose up -d

等 image build 完以及 service 起來以後,我們來利用指令安裝一個 Laravel 的環境。

sudo docker compose exec fpm composer create-project laravel/laravel .

接著我們打開 .env 檔案設定資料庫連線內容。

DB_CONNECTION=mysql
DB_HOST=host.docker.internal
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=homestead
DB_PASSWORD=secret

這時來測試一下連線。

sudo docker compose exec fpm php artisan migrate

因為防火牆擋住了,所以 connection 會 hang 住,最終失敗,接著來開始防火牆設定。

sudo ufw allow in on br-fpm to any port 3306

接著再執行一次 migrate 的指令。

sudo docker compose exec fpm php artisan migrate

   INFO  Preparing database.

  Creating migration table ............................................................................................................... 10ms DONE

   INFO  Running migrations.

  2014_10_12_000000_create_users_table .................................................................................................... 9ms DONE
  2014_10_12_100000_create_password_resets_table .......................................................................................... 2ms DONE
  2019_08_19_000000_create_failed_jobs_table .............................................................................................. 3ms DONE
  2019_12_14_000001_create_personal_access_tokens_table ................................................................................... 4ms DONE

可以看到 migration 順利成功了,如果發生 connection refuse 的錯誤的話,記得把 MySQL 的 bind-address 設定為 0.0.0.0

iptable 的話用下方的指令應可達到一樣的效果。

iptables -A INPUT -i br-fpm -p tcp --dport 3306 -j ACCEPT
iptables -A INPUT -p tcp --dport 3306 -j DROP

2022/10/18

Reload Nginx And Apache Config In Docker

如果在本機安裝 apache2 或 nginx 時想要 reload config 的話我們通常會使用下列的指令:

# nginx
nginx -s reload

# apache2
service httpd reload

但在 docker 的環境下就沒有那麼簡單,像 apache 的 reload 是把 script 寫在 service 的 config 裡面,不一定每個 image 裡面都有包含這個指令,CentOS 的我記得好像就沒有,為了要兼容所有的環境,我們可以利用 kill -USR1 這個方式套用到每個我們想要 reload without restart 的專案上面。

apache2

首先在根目錄建立兩個網頁檔:

  • ./www/1/index.html
  • ./www/2/index.html

各別會在網頁印出 1 以及 2 的內容識別。

docker-compose.yml

version: "3.9"

services:
  http:
    ports:
      - "80:80"
    container_name: httpd
    image: httpd
    restart: always
    volumes:
      - "./www:/www"
      - "./config/httpd:/usr/local/apache2/conf:ro"

我們建立了一個名為 http 的 container,使用 httpd 的 image,並且把 www 目錄掛進去,我事先把 httpd 的 config 複製出來以便在外面修改內容,接下來跑 docker compose up -d,打上 IP 後可以看到 httpd 傳統的首頁內容 It works!

接著我們修改 httpd.conf

DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">

# 改為

DocumentRoot "/www/1"
<Directory "/www/1">

我們先使用 docker compose restart 測試,執行後 refresh 網頁可以看到我們要的 1,接著再把 config 改成:

DocumentRoot "/www/2"
<Directory "/www/2">

這次我們使用 docker compose kill -s USR1 http ,此時我們可以看到 container 活得好好的,但 refresh 頁面後得到我們想要的 2 了,他的原理就是呼叫 container 內的 httpd daemon 去重新整理他的 service。

nginx

docker-compose.yml

version: "3.9"

services:
  http:
    ports:
      - "80:80"
    container_name: nginx
    image: nginx
    restart: always
    volumes:
      - "./www:/www"
      - "./config/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf:ro"

一樣,我們事前複製了 nginx 的 config 出來重新掛載回去,啟動 container 以後看見 nginx 的預設字樣 Welcome to nginx!

接著我們修改 defalut.conf

root   /usr/share/nginx/html;

# 改為

root   /www/1;

跟 apache2 一樣我們先使用 docker compose restart,可以順利看到 1 的結果,接著我們將 config 修改為:

root   /www/2;

使用 docker compose exec http nginx -s reload,我們就可以順利看到 2 了,nginx 跟 apache2 的 container 啟動方式不太一樣,所以不能用 kill 的方法,但事實上在 nginx 的 logrotate config 裡面他使用的方式也是:

kill -USR1 `cat /var/run/nginx.pid`

上面介紹了兩個主流網頁伺服器的重取 config 的目的是在當我們把 server 的 log 掛到我們本機目錄時,我們可能會需要依照情況在本機執行 logrotate 的動作,這樣就可以把 reload 腳本寫在本機的 logrotate config 的 postrotate/endscript 裡面了。

這邊附上 logrotate for docker 基本的配置:

nginx-docker

/YOUR_PATH_TO_LOG/*.log {
        daily
        missingok
        rotate 52
        compress
        delaycompress
        notifempty
        create 640 nginx adm
        sharedscripts
        postrotate
            sudo docker exec YOUR_CONTAINER_NAME nginx -s reload
        endscript
}

httpd-docker

/YOUR_PATH_TO_LOG/*.log {
        daily
        missingok
        rotate 14
        compress
        delaycompress
        notifempty
        sharedscripts
        postrotate
            sudo docker kill -s USR1 YOUR_CONTAINER_NAME 
        endscript
}

2022/10/10

Delete None Keeping Files By Command find

之前寫過 Delete None Keeping File 這篇,用來刪除固定數量的保存檔案,使用的是 ls,今天使用 find 來達成一樣的效果。

首先我們來建二十個以 0.05 秒間距不同的檔案:

for i in {1..20}; do touch "${i}.log"; sleep 0.05; done

列出檔案一下目前排序是這樣。

0 Oct 15 23:56 1.log
0 Oct 15 23:56 10.log
0 Oct 15 23:56 11.log
0 Oct 15 23:56 12.log
0 Oct 15 23:56 13.log
0 Oct 15 23:56 14.log
0 Oct 15 23:56 15.log
0 Oct 15 23:56 16.log
0 Oct 15 23:56 17.log
0 Oct 15 23:56 18.log
0 Oct 15 23:56 19.log
0 Oct 15 23:56 2.log
0 Oct 15 23:56 20.log
0 Oct 15 23:56 3.log
0 Oct 15 23:56 4.log
0 Oct 15 23:56 5.log
0 Oct 15 23:56 6.log
0 Oct 15 23:56 7.log
0 Oct 15 23:56 8.log
0 Oct 15 23:56 9.log

用預設的 find 查詢一下檔案內容。

find . -type f -name "*.log"

./10.log
./8.log
./19.log
./14.log
./13.log
./9.log
./2.log
./20.log
./1.log
./17.log
./6.log
./15.log
./12.log
./16.log
./4.log
./5.log
./11.log
./7.log
./18.log
./3.log

有人可能會說那利用 sort 不就可以自然排序正確的數字,沒錯,但那是因為我們的檔名自然有序,若遇到實際上沒有這樣結構的檔案,或者萬一臨時修改了檔名造成新排序不是我們要的結果就會出問題,所以最保險的方法還是利用日期去自然排序。

我們利用 -printf 這個指令來達到目的。

find . -type f -name "*.log" -printf "%T+ %p\n" | sort
2022-10-15+23:56:20.1777708840 ./1.log
2022-10-15+23:56:20.2297708850 ./2.log
2022-10-15+23:56:20.2817708850 ./3.log
2022-10-15+23:56:20.3337708860 ./4.log
2022-10-15+23:56:20.3857708860 ./5.log
2022-10-15+23:56:20.4457708870 ./6.log
2022-10-15+23:56:20.5057708880 ./7.log
2022-10-15+23:56:20.5697708880 ./8.log
2022-10-15+23:56:20.6337708890 ./9.log
2022-10-15+23:56:20.6817708890 ./10.log
2022-10-15+23:56:20.7377708900 ./11.log
2022-10-15+23:56:20.7977708910 ./12.log
2022-10-15+23:56:20.8457708910 ./13.log
2022-10-15+23:56:20.9057708920 ./14.log
2022-10-15+23:56:20.9577708920 ./15.log
2022-10-15+23:56:21.0217708930 ./16.log
2022-10-15+23:56:21.0977708940 ./17.log
2022-10-15+23:56:21.1617708950 ./18.log
2022-10-15+23:56:21.2137708950 ./19.log
2022-10-15+23:56:21.2617708960 ./20.log

printf 詳細用法可以參考 How to Use find -printf in Linux?,非常的強大,其他工作就可以接續 Delete None Keeping File 處理。

find . -type f -name "*.log" -printf "%T+ %p\n" | sort -r | awk '{print $NF}' | tail -n +11
./10.log
./9.log
./8.log
./7.log
./6.log
./5.log
./4.log
./3.log
./2.log
./1.log

上方指令搭配 xargs rm -f 就可以只保留最新的十份檔案。

2022/09/18

Supervisord

Supervisord 是 Linux 上常用的 daemon 監控工具,可以使用他來啟動並監控 daemon,若 daemon 因為任何的狀況消失,可以進行重啟並且記錄。

首先我們手動建立一個 daemon。

/tmp/test.sh
while true
do
    echo test
    sleep 1
done

接著來建立配置檔案。

/etc/supervisor/conf.d/test.conf
[program:test]
command = bash /tmp/test.sh
autostart = true
autorestart = true

配置完成後,執行相關指令使用。

$ sudo supervisorctl status
# 這個時候是空的

$ sudo supervisorctl reload
Restarted supervisord

$sudo supervisorctl status
test                             STARTING
# 我們可以看到剛才配置的 test.conf 已經生效了

$ ps -aux | grep test
root        5884  0.0  0.3   8696  3352 ?        S    01:27   0:00 bash /tmp/test.sh
# 利用 ps 來看他也確實在背景運作

$ sudo kill -9 5884
# 我們把這個 daemon process 砍掉

$ ps -aux | grep test
root        6107  0.0  0.3   8696  3468 ?        S    01:29   0:00 bash /tmp/test.sh
# 可以看到這個 daemon 重新啟動並且更換了 process id,表示原本的 process 有被我們砍掉,'但 supervisord 又把他啟動了

如果有添加新的 config,建議使用 sudo supervisorctl update 做單一配置更新,使用 reload 的話所有的 process 會重新啟動,如果是跑 queue 之類的 process 怕造成不可預期的後果。

配置內容

command=/bin/cat    ; 程式執行命令,建議使用絕對路徑。
process_name=%(program_name)s ; 程式名稱,可用的變數有 `group_name`, `host_node_name`, `process_num`, `program_name`, `here`(配置檔案目錄)。 一般程式需要執行多個副本的情況會使用。後面會有例子。
numprocs=1     ; 程式執行的副本個數,預設為1,如果值大於1,則`process_name` 必須包含 `%(process_num)s`
numprocs_start=0    ; `%(process_num)s`起始數字,預設為0
00=/tmp    ; 程式執行的所在目錄,相當於先cd到指定目錄,然後執行程式。
umask=022      ; umask for process (default None)
priority=999     ; 程式操作的的優先順序,例如在start all/stop all,高優先順序的程式會先關閉和重啟。
autostart=true    ; 在supervisord啟動時自動啟動,預設為true
startsecs=1     ; 程式啟動前等待時間等待時間。預設為1。
startretries=3    ; 嘗試重啟最大次數。預設為3。
autorestart=unexpected  ; 是否自動重啟,可選引數為 false, unexpected, true。如果為false則不自動重啟,如果為unexpected表示如果程式退出訊號不在 `exitcodes` 中,則自動重啟。預設為unexpected
exitcodes=0,2     ; 程式退出碼。配合`autorestart`使用。預設為 0,2
stopsignal=QUIT    ; 殺死程序是傳送的訊號,預設為TREM。
stopwaitsecs=10    ; 傳送SIGKILL訊號前最大等待時間。預設為10。
user       ; 以指定使用者身份啟動程式。預設為當前使用者。
stopasgroup=false    ; 是否向子程序傳送停止訊號,這對於Flask的debug模式很有用處,如果設定為true,則不向子程序傳送停止訊號。預設為false
killasgroup=false    ; 是否向子程序傳送kill訊號,預設為false
redirect_stderr=false   ; 將錯誤輸出定向到標準輸出,預設為false
stdout_logfile=/a/path  ; 標準輸出日誌路徑,可選引數為 `自定義` `AUTO` `NONE`,`自定義`將日誌寫到自定義路徑,可用的變數有`group_name`, `host_node_name`, `process_num`, `program_name`, `here`(配置檔案目錄);`NONE`不建立日誌;`AUTO` 又supervisord自動選擇路徑,並且當supervisord服務重新啟動時原來自動建立的日誌以及日誌的備份檔案會被刪除。預設為AUTO
stdout_logfile_maxbytes=1MB ; 標準輸出日誌單個檔案最大大小,如果超過指定大小會將日誌檔案備份,可用的單位 KB MB GB。如果設定為0則表示不限制檔案大小。預設為50MB
stdout_logfile_backups=10  ; 標準輸出日誌檔案最大備份數。預設為10
stdout_capture_maxbytes=1MB ; 當程序處於“stdout capture mode”模式下寫入到FIFO佇列最大位元組數,可用單位 KB MB GB。預設為0,詳細說明見[capture-mode](http://supervisord.org/logging.html#capture-mode)
stdout_events_enabled=false ; 
;以下配置項配置錯誤輸出的日誌引數。和上面標準輸出配置相同。
stderr_logfile=/a/path  ;
stderr_logfile_maxbytes=1MB ;
stderr_logfile_backups=10  ;
stderr_capture_maxbytes=1MB ;
stderr_events_enabled=false ;
environment=A="1",B="2"  ; 環境變數設定,可用的變數有 `group_name`, `host_node_name`, `process_num`, `program_name`, `here`。 預設為空。
serverurl=AUTO    ; override serverurl computation (childutils)

參考網站

2022/07/06

Compile Apache and OpenSSL

OS: CentOS 8
Target:

  • PHP 7.4
  • PHP-FPM
  • Apache 2.4.46
  • OpenSSL 1.1.1g

Start compile

sudo dnf install install -y gcc gcc-c++ make prec-devel libtool perl-core zlib-devel

# Compile apr
sudo ./configure --prefix=/usr/local/apr
sudo make
sudo make install

# Compile apr-util
sudo ./configure --prefix=/usr/local/apr-util --with-apr=/usr/local/apr
sudo make
sudo make install

# Compile openssl
sudo ./config --prefix=/usr/local/openssl --openssldir=/usr/local/openssl shared zlib
sudo make
sudo make install

# Compile apache
sudo ./configure --prefix=/usr/local/apache2446 --enable-so --enable-ssl --enable-cgi --enable-rewrite --enable-modules=most --enable-mpms-shared=all --with-mpm=prefork --with-zlib --with-apr=/usr/local/apr --with-apr-util=/usr/local/apr-util --with-ssl=/usr/local/openssl
sudo make
sudo make install

# Apache system configuration
# Add Apache executables to PATH ( You can use nano instead of vi)
# create and open the following file
sudo vim /etc/profile.d/httpd.sh
  
# paste the following content, save and exit
pathmunge /usr/local/apache2446/bin

# Add Systemd entry

# create and open the following file
sudo vim /etc/systemd/system/httpd.service

paste the following content, save and exit.

[Unit]
Description=The Apache HTTP Server
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/apache2446/bin/apachectl -k start
ExecReload=/usr/local/apache2446/bin/apachectl -k graceful
ExecStop=/usr/local/apache2446/bin/apachectl -k graceful-stop
PIDFile=/usr/local/apache2446/logs/httpd.pid
PrivateTmp=true

[Install]
WantedBy=multi-user.target
# reload the systemctl daemon
sudo systemctl daemon-reload

# start Apache httpd server (ignore the warnings at this stage)
sudo systemctl start httpd
sudo systemctl enable httpd

# Install PHP
sudo dnf install -y http://rpms.remirepo.net/enterprise/remi-release-8.rpm
dnf module reset php
dnf module install -y php:remi-7.4

sudo service php-fpm start
sudo systemctl enable php-fpm

Reference

2022/06/10

Shell Script With Optional Arguments

我們在操作 Linux 上的 script 時,常常會使用一些 optional arguments,例如:

ls -l
grep -iR --include *.php

我們自己製作腳本時可以使用 getopts 以及 shift 達到這樣的效果,以一個安裝系統的腳本舉例。

install.sh

#/bin/bash

install_nginx=0
install_mysql=0
nginx_port=80
mysql_port=3306

while getopts "nmP:p:" OPTION; do
  case "$OPTION" in
  n)
    install_nginx=1
    ;;
  m)
    install_mysql=1
    ;;
  p)
    nginx_port="$OPTARG"
    ;;
  P)
    mysql_port="$OPTARG"
    ;;
  esac
done

echo "Install basic"

if [ "$install_nginx" == 1 ]; then
  echo "Install nginx with port $nginx_port"
fi

if [ "$install_mysql" == 1 ]; then
  echo "Install mysql with port $mysql_port"
fi
bash install.sh

# Install basic

bash install.sh -n

# Install basic
# Install nginx with port 80

bash install.sh -n -p 8888 -m
# Install basic
# Install nginx with port 8888
# Install mysql with port 3306

上面的方法只能帶入單一字符,所以像是 port 這種常見共用的文字只能用大小寫或者不同字符取代,彈性比較少,使用 shift 可以解決這個問題。

#/bin/bash

install_nginx=0
install_mysql=0
nginx_port=80
mysql_port=3306

while [ "$1" != "" ]; do
  case "$1" in
  --nginx)
    install_nginx=1
    ;;
  --nginx-port)
    shift
    nginx_port="$1"
    ;;
  --mysql)
    install_mysql=1
    ;;
  --mysql-port)
    shift
    mysql_port="$1"
    ;;
  esac
  shift
done

echo "Install basic"

if [ "$install_nginx" == 1 ]; then
  echo "Install nginx with port $nginx_port"
fi

if [ "$install_mysql" == 1 ]; then
  echo "Install mysql with port $mysql_port"
fi
bash install.sh --nginx
# Install basic
# Install nginx with port 80

bash install.sh --nginx --nginx-port 8888
# Install basic
# Install nginx with port 8888

bash install.sh --nginx --mysql --mysql-port 3307
# Install basic
# Install nginx with port 80
# Install mysql with port 3307

reference

2022/03/02

Linux Fun Command

網路上有一些方便的指令,會在這邊持續的更新做為筆記

設定 history 顯示時間
# 使用 history 可以查看之前下了什麼指令,但預設是沒有顯示時間的,可以透過設定來決定時間格式

$ HISTTIMEFORMAT="%Y-%m-%d %T "
$ HISTTIMEFORMAT="%F %T: " # 跟上面結果相同

# 想要永久存在的話就是去 .bashrc 或 .profile 設定

export HISTTIMEFORMAT="%Y-%m-%d %T "
檢查 port 是否存在
# 一般人會用這個指令,但我覺得他不是「專門」來做檢查這件事的
$ telnet cha15.info 21

# 使用nc 的話,會得到正確的資訊
$ nc -zv chan15.info 80
$ Connection to chan15.info 80 port [tcp/http] succeeded!

# 他同時也支援同時檢查多個 port
$ nc -zv chan15.info 80 21 33
$ Connection to chan15.info 80 port [tcp/http] succeeded!
$ Connection to chan15.info 21 port [tcp/ftp] succeeded!
$ nc: connect to chan15.info port 33 (tcp) failed: Connection refused
一次寫入多行資料
$ cat <<eot > test.txt
> 1
> 2
> 3
> eot
搜尋語取代檔案文字
grep -rl 'windows' | xargs sed -i 's/windows/linux/g'
刪除超過三十天的檔案
find PATH -ctime +30 | xargs -f
nmap

之前會用 nc 檢查 service,用 nmap 來做更方便

$ nmap localhost

Starting Nmap 7.01 ( https://nmap.org ) at 2019-05-03 15:05 CST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.0000080s latency).
Not shown: 995 closed ports
PORT    STATE SERVICE
21/tcp  open  ftp
22/tcp  open  ssh
25/tcp  open  smtp
80/tcp  open  http
443/tcp open  https
du 特殊用法

一般使用 du 的話他會列出你當前目錄每一個層級的所有的檔案內容跟大小,不過其實這樣不太實用,我們通常只要在第一層知道檔案大小即可

$ du -h -d 1
12K     ./apm
24K     ./wireshark
16K     ./cron.d
20K     ./apport
8.0K    ./cron.hourly
40K     ./sysctl.d

如果希望可以照檔案大小排序的話

$ du -h -d 1 | sort -h
4.0K    ./binfmt.d
4.0K    ./landscape
4.0K    ./libpaper.d
4.0K    ./modules-load.d
4.0K    ./opt
4.0K    ./pki

$ du -h -d 1 | sort -h -r
8.7M    .
880K    ./apache2
756K    ./ssl
668K    ./apparmor.d
440K    ./php
400K    ./init
368K    ./init.d

2022/01/27

Special Variable In Linux

Linux 有許多特殊變數可以使用,要用的時候常常不記得,用這篇統整記錄一下,以後用到其他的會持續更新。

name description
-G 存在﹐並且由 GID 所執行的行程所擁有。
-L 存在﹐並且是 symbolic link 。
-O 存在﹐並且由 UID 所執行的行程所擁有。
-S 存在﹐並且是一個 socke 。
-b 存在﹐並且是 block 檔案﹐例如磁碟等。
-c 存在﹐並且是 character 檔案﹐例如終端或磁帶機。
-d 存在﹐並且是一個目錄。
-e 存在。
-f 存在﹐並且是一個檔案。
-g 存在﹐並且有 SGID 屬性。
-k 存在﹐並且有 sticky bit 屬性。
-p 存在﹐並且是用於行程間傳送資訊的 name pipe 或是 FIFO。
-r 存在﹐並且是可讀的。
-s 存在﹐並且體積大於 0 (非空檔)。
-u 存在﹐並且有 SUID 屬性。
-w 存在﹐並且可寫入。
-x 存在﹐並且可執行。
-z 空字串
-n 非空字串

整數比較

name description example
-eq 等於 [ integer1 -eq integer2 ]
-ne 不等於 [ integer1 -ne integer2 ]
-lt 小於 [ integer1 -lt integer2 ]
-gt 大於 [ integer1 -gt integer2 ]
-le 小於或等於 [ integer1 -le integer2 ]
-ge 大於或等於 [ integer1 -ge integer2 ]

特殊變量

name description
$1~$9 函數的第一個到第9個的參數
$0 函數所在的腳本名
$# 函數的參數總數
$* 函數的全部參數
$? 顯示最後命令的退出狀態,0 表示沒有錯誤,其他表示有錯誤
$$ 腳本運行的當前 process id
$! 上一個執行命令的 process id
$_ 在此之前執行的命令或者腳本的最後一個參數
test.sh
#!/bin/bash

echo $0
echo $1
echo $2
echo $#
echo $*
echo $$
bash test.sh a b
test.sh
a
b
2
a b
54877

$? 這個預設變數非常實用,可以用來做許多沒有給足判斷的工具當作一種判斷依據,以這篇 tmux 的範例為例,tmux 的 has-session 並非回傳 boolean,而是 session 存在則無反應,session 不存在時噴出 can’t find session 訊息,因此你無法下這種語法:

if tmux has-session -t "$name"; then
    # do something
fi

取而代之的方法就如同我範例那樣抓最後一個 process 的 exit code 當作判斷依據。

$$ 則是如果該 bash 為 daemon 時,可以將 pid 存放在某個檔案內,這樣就可以偵測如果該 pid 有檔案時先 kill 該 process 之後在重啟,否則同一時間就會同時有兩個 daemon 在執行。

2021/12/02

Laravel Connet To MSSQL Server On CentOS

PHP 安裝

# Install PHP 7.4
sudo yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo yum -y install https://rpms.remirepo.net/enterprise/remi-release-7.rpm
sudo yum -y install yum-utils
sudo yum-config-manager --enable remi-php74
sudo yum install -y php php-pdo php-odbc php-mbstring php-zip php-xml

MSSQL Driver 安裝,擇一安裝

1. MSSQL Driver(首選,支援版本 SQL Server 2000 以上)

sudo yum install php-sqlsrv
curl https://packages.microsoft.com/config/rhel/7/prod.repo > /etc/yum.repos.d/mssql-release.repo

sudo yum remove unixODBC-utf16 unixODBC-utf16-devel #to avoid conflicts
sudo ACCEPT_EULA=Y yum install -y msodbcsql17

2. FreeTDS(Sql Server 2000)

sudo yum -y install php-mssql
sudo yum -y install unixODBC-*
sudo yum -y install freetds

cd /usr/lib64
sudo mv libtdsodbc.so.0.0.0 libtdsodbc.so
sudo vi /etc/odbcinst.ini

在最後增加:

[FreeTDS]
Description = ODBC for FreeTDS
Driver64    = /usr/lib64/libtdsodbc.so
Setup64     = /usr/lib64/libtdsodbc.so
FileUsage   = 1
sudo vi /etc/freetds.conf

在最後增加:

[SqlServer2000]
    host = {IP}
    port = 1433
    tds version = 8.0
    client charset = UTF-8

在 Laravel 的配置一切照舊,除了 MSSQL 的 host 要指到 SqlServer2000,若遇到時間格式的問題可以參考此篇,使用 FreeTDS 可以通吃,但怕會有無法預期的問題,不建議同一台 Server 同時使用兩種方式。

參考網站

2021/05/05

Vagrant provision with docker and docker-compose install

vagrant 提供了 provision 初始化功能,可以類似 Dockerfile 在初始化 vagrant 的時候就把想要安裝的內容處理好,下面示範一下如何安裝一個乾淨的 ubuntu 後把 docker 以及 docker-compose 一併安裝。

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"

  config.vm.provider "virtualbox" do |vb|
    vb.memory = "1024"
  end

  config.vm.provision "shell", inline: <<-SHELL
    docker -v &> /dev/null

    if [[ $? != 0 ]]; then
      echo "start install docker"
      curl -sL https://get.docker.com/ | sudo bash
      sudo usermod -aG docker vagrant
      docker -v
    else
      echo "docker installed"
    fi

    docker-compose -v &> /dev/null

    if [[ $? != 0 ]]; then
      echo "start install docker-compose"
      sudo curl -sL "https://github.com/docker/compose/releases/download/1.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
      sudo chmod +x /usr/local/bin/docker-compose
      sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
      docker-compose -v
    else
      echo "docker-compose installed"
    fi
  SHELL
end

由於 provision 有可能重複使用,所以最一些執行檢查是必要的,這邊我檢查已安裝的技巧是使用 -v 這個參數,如果沒有安裝,系統會報錯,因此 $? 會非 0,藉此判斷有沒有安裝過。

2020/12/22

Delete None Keeping Files

當我們在備份文件的時候,通常只保留一定的數量,例如說 10 天份,我們可能會這樣寫:

$ find PATH_TO_FILES -mtime +30 | xargs rm -f &> /dev/null
$ find PATH_TO_FILES -mtime +30 -exec rm {} \ &> /dev/null;

這兩種寫法結果一樣,都是會刪除建立超過三十天以上的檔案,不過這會有個問題,假設今天因為某些因素備份指令中斷了,但 crontab 依舊在跑刪除指令,若 op 沒有及時發現,備份檔終究會被刪光,因此另一種寫法我認為是比較好的

我們先製造六個檔案

i=1; while [ $i -lt 7 ]; do touch $i; ((i++)); done

$ ll -d ./*
-rw-r--r-- 1 root root 0 12月 22 11:45 ./1
-rw-r--r-- 1 root root 0 12月 22 11:45 ./2
-rw-r--r-- 1 root root 0 12月 22 11:45 ./3
-rw-r--r-- 1 root root 0 12月 22 11:45 ./4
-rw-r--r-- 1 root root 0 12月 22 11:45 ./5
-rw-r--r-- 1 root root 0 12月 22 11:45 ./6

假設我們只要保留 5 個檔案,指令就是:

$ ls -ltd ./* | tail -n +6 | awk '{print $NF}' | xargs rm -f &> /dev/null

-d 過濾了 ./ 以及 ../-t 會照時間排序,tail -n +6 就是拉出超過 n 數量的結果,要保留 5 份的話要填入 6,所以檔名 1-6 時 6 會被篩選出來,awk '{print $NF}' 會印出篩選出來資訊的尾端,也就是檔名,最後就用 xargs rm -f 刪除該檔案,這樣寫的話不管時間的影響該備份目錄永遠會留下你指定的檔案數量。

2020/11/11

awk search for specific key

一般我們的 envfile 會這樣配。

NAME=chan
AGE=40

假設今天 envfile 很長,你只想找出某個 key 的值該怎麼做呢,可以透過 awk

search.sh
#!/bin/bash

DIR=$1
KEY=$2

awk -F= -v key="$KEY" '{if ($1 == key) {print $2}}' $DIR

執行

bash search.sh envfile NAME

會得到 chan,補上變數跟目錄是否存在檢查就更完整了。

2020/01/15

jq

JSON 格式的應用現在已經隨處可見了,目前使用的程式語言都有相關支援的套件,如果要在 Linux 底下操作的話有一個非常好用的工具叫 jq,下面來展示應用範例

應用網址:https://jsonplaceholder.typicode.com/users

curl -s https://jsonplaceholder.typicode.com/users | jq .

我們會得到格式漂亮的結果

[
  {
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": {
      "street": "Kulas Light",
      "suite": "Apt. 556",
      "city": "Gwenborough",
      "zipcode": "92998-3874",
      "geo": {
        "lat": "-37.3159",
        "lng": "81.1496"
      }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
      "name": "Romaguera-Crona",
      "catchPhrase": "Multi-layered client-server neural-net",
      "bs": "harness real-time e-markets"
    }
  }
]

而 jq 可以做 node 的搜尋,以上面那个例子

curl -s https://jsonplaceholder.typicode.com/users | jq .[0].name # "Leanne Graham"
curl -s https://jsonplaceholder.typicode.com/users | jq .[0].address.street # "Kulas Light"

使用 jq 的一些參數也可以達成 json encode 的功能:

test

	#!/bin/bash
 
NAME=$1
AGE=$2
 
JSON_STRING=$(jq -n \
    --arg name "$NAME" \
    --arg age "$AGE" \
    '{name: $name, age: $age}')
 
echo $JSON_STRING

./test chan 40 # { "name": "chan", "age": "40" }

jq 沒有驗證 json format 的功能,但我們可以利用一些 shell script 的特性辦到這件事

#!/bin/bash
 
function json_validator() {
    echo $1 | jq . &> /dev/null
 
    if [[ $? == 0 ]]; then
        echo "$1 is valid format"
    else
        echo "$1 is invalid format"
    fi
}
 
JSON='{"name": "chan"}'
json_validator "$JSON"
# {"name": "chan"} is valid format
 
JSON='{"name":}'
json_validator "$JSON"
# {"name":} is invalid format

jq 也可以使用自定義模組,在 ~/.jq 裡面可以將一些複雜但常用的內容先寫好

def blurry($f):
  ($f | ascii_upcase) as $ucf
  | to_entries[]
  | select((.key|ascii_upcase) | startswith($ucf))
  | .value;
 
 
def very_blurry($f):
  ($f | ascii_upcase) as $ucf
  | to_entries[]
  | select(.key | ascii_upcase | index($ucf))
  | .value;

這樣的話像搜尋 docker 的 config 的話就非常好用,像是 docker inspect test | jq '.. | blurry("config")?' | objects

搜尋語法

$ jq 'map(select(.id=="1234"))'
$ jq 'map(select(.id|index("1234")))'
$ jq 'map(select(.id|contains("1234")))'
$ jq 'map(select(.id|test("1234";"i")))'
$ jq 'map(select(.id|match("1234";"i")))'

docker inspect 出來的結果是 json format,假設我們今天要找 LogPath,但不記得他的層級的話,有個語法可以針對該 key 找出資料。

docker inspect <container_id> | jq -r '.. | .LogPath? // empty'

-r 是輸出 raw data 好複製,// empty 部分會讓沒搜到的結構不輸出,因此最終只會跑出我們要的結果。