簡體   English   中英

如何從 docker 容器在主機上運行 shell 腳本?

[英]How to run shell script on host from docker container?

如何從 docker 容器控制主機?

例如,如何執行復制到主機 bash 腳本?

使用命名管道。 在主機操作系統上,創建一個腳本來循環和讀取命令,然后調用eval

讓 docker 容器讀取到該命名管道。

為了能夠訪問管道,您需要通過卷安裝它。

這類似於 SSH 機制(或類似的基於套接字的方法),但將您正確地限制在主機設備上,這可能更好。 另外,您不必傳遞身份驗證信息。

我唯一的警告是要小心你為什么這樣做。 如果您想創建一種使用用戶輸入或其他方式進行自我升級的方法,這完全是一件事情,但您可能不想調用命令來獲取一些配置數據,因為正確的方法是將其傳遞為args/volume 進入 docker。 另外,請注意您正在評估的事實,因此請考慮一下權限模型。

其他一些答案,例如運行腳本。 由於它們無法訪問完整的系統資源,因此通常無法在卷下工作,但根據您的使用情況,它可能更合適。

這個答案只是Bradford Medeiros 解決方案的更詳細版本,對我來說這也是最好的答案,所以歸功於他。

在他的回答中,他解釋了要做什么(命名管道),但不完全解釋如何去做。

我不得不承認,當我閱讀他的解決方案時,我不知道什么是命名管道。 所以我努力實現它(雖然它實際上很簡單),但我確實成功了。 所以我回答的重點只是詳細說明您需要運行的命令才能使其正常工作,但再次感謝他。

第 1 部分 - 在沒有 docker 的情況下測試命名管道概念

在主主機上,選擇要放置命名管道文件的文件夾,例如/path/to/pipe/和管道名稱,例如mypipe ,然后運行:

mkfifo /path/to/pipe/mypipe

管道已創建。 類型

ls -l /path/to/pipe/mypipe 

並檢查以“p”開頭的訪問權限,如

prw-r--r-- 1 root root 0 mypipe

現在運行:

tail -f /path/to/pipe/mypipe

終端現在正在等待將數據發送到此管道

現在打開另一個終端窗口。

然后運行:

echo "hello world" > /path/to/pipe/mypipe

檢查第一個終端(帶有tail -f的終端),它應該顯示“hello world”

第 2 部分 - 通過管道運行命令

在主機容器上,不要運行僅輸出作為輸入發送的任何內容的tail -f ,而是運行以下命令,將其作為命令執行:

eval "$(cat /path/to/pipe/mypipe)"

然后,從另一個終端嘗試運行:

echo "ls -l" > /path/to/pipe/mypipe

返回第一個終端,您應該會看到ls -l命令的結果。

第 3 部分 - 讓它永遠聆聽

您可能已經注意到,在上一部分中,在顯示ls -l輸出之后,它會停止偵聽命令。

而不是eval "$(cat /path/to/pipe/mypipe)" ,運行:

while true; do eval "$(cat /path/to/pipe/mypipe)"; done

(你可以不知道)

現在您可以一個接一個地發送無限數量的命令,它們都將被執行,而不僅僅是第一個。

第 4 部分 - 即使在重新啟動時也能正常工作

唯一需要注意的是,如果主機必須重新啟動,“while”循環將停止工作。

要處理重新啟動,這里我做了什么:

while true; do eval "$(cat /path/to/pipe/mypipe)"; done while true; do eval "$(cat /path/to/pipe/mypipe)"; done 在帶有#!/bin/bash標頭的名為execpipe.sh的文件中while true; do eval "$(cat /path/to/pipe/mypipe)"; done

不要忘記chmod +x

通過運行將其添加到 crontab

crontab -e

然后添加

@reboot /path/to/execpipe.sh

在這一點上,測試它:重啟你的服務器,當它備份時,將一些命令回顯到管道中並檢查它們是否被執行。 當然,您無法看到命令的輸出,因此ls -l無濟於事,但touch somefile會有所幫助。

另一種選擇是修改腳本以將輸出放入文件中,例如:

while true; do eval "$(cat /path/to/pipe/mypipe)" &> /somepath/output.txt; done

現在您可以運行ls -l並且輸出(在 bash 中使用&>的 stdout 和 stderr)應該在 output.txt 中。

第 5 部分 - 使其與 docker 一起工作

如果您像我一樣同時使用 docker compose 和 dockerfile,這就是我所做的:

假設您要將 mypipe 的父文件夾掛載為容器中的/hostpipe

添加這個:

VOLUME /hostpipe

在您的 dockerfile 中以創建掛載點

然后添加這個:

volumes:
   - /path/to/pipe:/hostpipe

在您的 docker compose 文件中,以便將 /path/to/pipe 掛載為 /hostpipe

重新啟動 docker 容器。

第 6 部分 - 測試

執行到您的 docker 容器中:

docker exec -it <container> bash

進入掛載文件夾並檢查您是否可以看到管道:

cd /hostpipe && ls -l

現在嘗試從容器中運行命令:

echo "touch this_file_was_created_on_main_host_from_a_container.txt" > /hostpipe/mypipe

它應該工作!

警告:如果你有一個 OSX (Mac OS) 主機和一個 Linux 容器,它不會工作(解釋在這里https://stackoverflow.com/a/43474708/10018801並在這里發布https://github.com/docker /for-mac/issues/483 )因為管道實現不一樣,所以你從Linux寫入管道的內容只能由Linux讀取,而你從Mac OS寫入管道的內容只能由a讀取Mac OS(這句話可能不太准確,但請注意存在跨平台問題)。

例如,當我從我的 Mac OS 計算機在 DEV 中運行我的 docker setup 時,如上所述的命名管道不起作用。 但在登台和生產中,我有 Linux 主機和 Linux 容器,而且它運行良好。

第 7 部分 - Node.JS 容器中的示例

以下是我如何從 Node.JS 容器向主主機發送命令並檢索輸出:

const pipePath = "/hostpipe/mypipe"
const outputPath = "/hostpipe/output.txt"
const commandToRun = "pwd && ls-l"

console.log("delete previous output")
if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath)

console.log("writing to pipe...")
const wstream = fs.createWriteStream(pipePath)
wstream.write(commandToRun)
wstream.close()

console.log("waiting for output.txt...") //there are better ways to do that than setInterval
let timeout = 10000 //stop waiting after 10 seconds (something might be wrong)
const timeoutStart = Date.now()
const myLoop = setInterval(function () {
    if (Date.now() - timeoutStart > timeout) {
        clearInterval(myLoop);
        console.log("timed out")
    } else {
        //if output.txt exists, read it
        if (fs.existsSync(outputPath)) {
            clearInterval(myLoop);
            const data = fs.readFileSync(outputPath).toString()
            if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath) //delete the output file
            console.log(data) //log the output of the command
        }
    }
}, 300);

我使用的解決方案是通過SSH連接到主機並執行如下命令:

ssh -l ${USERNAME} ${HOSTNAME} "${SCRIPT}"

更新

由於這個答案不斷獲得投票,我想提醒(並強烈推薦),用於調用腳本的帳戶應該是一個完全沒有權限的帳戶,但只能將該腳本作為sudo執行(可以從sudoers文件中完成)。

更新:命名管道

我上面建議的解決方案只是我對 Docker 比較陌生時使用的解決方案。 現在在 2021 年,看看有關命名管道的答案。 這似乎是一個更好的解決方案。

但是,那里沒有人提到安全性 將評估通過管道發送的命令的腳本(調用eval的腳本)實際上不能eval用於整個管道輸出,而是處理特定情況並根據發送的文本調用所需的命令,否則任何可以做任何事情都可以通過管道發送。

這真的取決於你需要那個 bash 腳本來做什么!

例如,如果 bash 腳本只是回顯一些輸出,你可以這樣做

docker run --rm -v $(pwd)/mybashscript.sh:/mybashscript.sh ubuntu bash /mybashscript.sh

另一種可能性是您希望 bash 腳本安裝一些軟件——比如安裝 docker-compose 的腳本。 你可以做類似的事情

docker run --rm -v /usr/bin:/usr/bin --privileged -v $(pwd)/mybashscript.sh:/mybashscript.sh ubuntu bash /mybashscript.sh

但是在這一點上,您真的必須深入了解腳本正在做什么以允許從容器內部對您的主機所需的特定權限。

我的懶惰使我找到了未在此處作為答案發布的最簡單的解決方案。

它基於luc juggery精彩文章

為了從 docker 容器中獲得 linux 主機的完整 shell,您需要做的就是:

docker run --privileged --pid=host -it alpine:3.8 \
nsenter -t 1 -m -u -n -i sh

解釋:

--privileged :授予容器額外的權限,它允許容器訪問主機的設備(/dev)

--pid=host :允許容器使用 Docker 主機(運行 Docker 守護進程的虛擬機)的進程樹 nsenter 實用程序:允許在現有命名空間(為容器提供隔離的構建塊)中運行進程

nsenter (-t 1 -m -u -n -i sh) 允許在與 PID 1 的進程相同的隔離上下文中運行進程 sh。然后整個命令將在 VM 中提供交互式 sh shell

此設置具有重大安全隱患,應謹慎使用(如果有)。

編寫一個簡單的服務器 python 服務器偵聽端口(比如 8080),將端口 -p 8080:8080 與容器綁定,向 localhost:8080 發出 HTTP 請求以詢問 python 服務器運行帶有 popen 的 shell 腳本,運行 curl 或編寫代碼以發出 HTTP 請求 curl -d '{"foo":"bar"}' localhost:8080

#!/usr/bin/python
from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer
import subprocess
import json

PORT_NUMBER = 8080

# This class will handles any incoming request from
# the browser 
class myHandler(BaseHTTPRequestHandler):
        def do_POST(self):
                content_len = int(self.headers.getheader('content-length'))
                post_body = self.rfile.read(content_len)
                self.send_response(200)
                self.end_headers()
                data = json.loads(post_body)

                # Use the post data
                cmd = "your shell cmd"
                p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
                p_status = p.wait()
                (output, err) = p.communicate()
                print "Command output : ", output
                print "Command exit status/return code : ", p_status

                self.wfile.write(cmd + "\n")
                return
try:
        # Create a web server and define the handler to manage the
        # incoming request
        server = HTTPServer(('', PORT_NUMBER), myHandler)
        print 'Started httpserver on port ' , PORT_NUMBER

        # Wait forever for incoming http requests
        server.serve_forever()

except KeyboardInterrupt:
        print '^C received, shutting down the web server'
        server.socket.close()

如果您不擔心安全性並且您只是想從另一個 docker 容器(如 OP)中啟動主機上的 docker 容器,您可以通過共享它的偵聽套接字將主機上運行的 docker 服務器與 docker 容器共享。

請參閱https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface並查看您的個人風險承受能力是否允許此特定應用程序。

您可以通過將以下卷參數添加到您的啟動命令來做到這一點

docker run -v /var/run/docker.sock:/var/run/docker.sock ...

或通過在您的 docker compose 文件中共享 /var/run/docker.sock ,如下所示:

version: '3'

services:
   ci:
      command: ...
      image: ...
      volumes:
         - /var/run/docker.sock:/var/run/docker.sock

當您在 docker 容器中運行 docker start 命令時,在您的主機上運行的 docker 服務器將看到請求並配置同級容器。

信用:http: //jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/

docker run --detach-keys="ctrl-p" -it -v /:/mnt/rootdir --name testing busybox
# chroot /mnt/rootdir
# 

我有一個簡單的方法。

第 1 步:掛載 /var/run/docker.sock:/var/run/docker.sock (這樣您就可以在容器內執行 docker 命令)

第 2 步:在您的容器中執行以下操作。 這里的關鍵部分是( --network host因為這將從主機上下文執行)

docker run -i --rm --network host -v /opt/test.sh:/test.sh alpine:3.7 sh /test.sh

test.sh 應該包含一些你需要的命令(ifconfig、netstat 等)。 現在您將能夠獲得主機上下文輸出。

正如 Marcus 所提醒的,docker 基本上是進程隔離。 從 docker 1.8 開始,您可以在主機和容器之間雙向復制文件,請參閱docker cp的文檔

https://docs.docker.com/reference/commandline/cp/

復制文件后,您可以在本地運行它

您可以使用管道概念,但使用主機上的文件和fswatch來完成從 docker 容器在主機上執行腳本的目標。 像這樣(使用風險自負):

#! /bin/bash

touch .command_pipe
chmod +x .command_pipe

# Use fswatch to execute a command on the host machine and log result
fswatch -o --event Updated .command_pipe | \
            xargs -n1 -I "{}"  .command_pipe >> .command_pipe_log  &

 docker run -it --rm  \
   --name alpine  \
   -w /home/test \
   -v $PWD/.command_pipe:/dev/command_pipe \
   alpine:3.7 sh

rm -rf .command_pipe
kill %1

在此示例中,在容器內將命令發送到 /dev/command_pipe,如下所示:

/home/test # echo 'docker network create test2.network.com' > /dev/command_pipe

在主機上,您可以檢查網絡是否已創建:

$ docker network ls | grep test2
8e029ec83afe        test2.network.com                            bridge              local

在我的場景中,我只是在容器中 ssh 登錄主機(通過主機 ip),然后我可以對主機做任何我想做的事情

我發現使用命名管道的答案很棒。 但我想知道是否有辦法獲得執行命令的 output。

解決方案是創建兩個命名管道:

mkfifo /path/to/pipe/exec_in
mkfifo /path/to/pipe/exec_out

然后,@Vincent 建議的使用循環的解決方案將變為:

# on the host
while true; do eval "$(cat exec_in)" > exec_out; done

然后在 docker 容器上,我們可以執行命令並使用以下命令獲取 output:

# on the container
echo "ls -l" > /path/to/pipe/exec_in
cat /path/to/pipe/exec_out

如果有人感興趣,我需要在容器上的主機上使用故障轉移 IP,我創建了這個簡單的 ruby 方法:

def fifo_exec(cmd)
  exec_in = '/path/to/pipe/exec_in'
  exec_out = '/path/to/pipe/exec_out'
  %x[ echo #{cmd} > #{exec_in} ]
  %x[ cat #{exec_out} ]
end
# example
fifo_exec "curl https://ip4.seeip.org"

要擴展user2915097 的響應

隔離的想法是能夠非常清楚地限制應用程序/進程/容器(無論您的角度是什么)可以對主機系統執行的操作。 因此,能夠復制和執行文件將真正打破整個概念。

是的。 但有時它是必要的。

不,事實並非如此,或者 Docker 不是正確使用的東西。 您應該做的是為您想要做的事情聲明一個清晰的接口(例如更新主機配置),並編寫一個最小的客戶端/服務器來完全做到這一點,僅此而已。 然而,一般來說,這似乎不是很理想。 在許多情況下,您應該簡單地重新考慮您的方法並消除這種需求。 當基本上所有東西都是可以使用某種協議訪問的服務時,Docker 就應運而生了。 我想不出任何合適的 Docker 容器獲得在主機上執行任意內容的權利的用例。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM