เขียนโค๊ดกับ VS Code บน Docker อย่างไร้รอยต่อด้วย Remote Containers

Nuttavut Thongjor

ปฏิเสธได้ยากว่ายุค 4.0 จะไม่มีใครใช้ Docker และยากยิ่งกว่าที่ความชัง Microsoft จะทำให้รังเกียจ Editor เทพทรูอย่าง Visual Studio Code (VS Code)

แม้ VS Code จะเทพทรู แต่เธอว์ก็ยังเปย์ไม่พอที่จะใช้งานกับ container ได้อย่างไร้รอยต่อ ตัวอย่างสุดคลาสสิคเช่น มีโค้ดอยู่บนเครื่องเรา แต่ติดตั้ง lib ต่าง ๆ ไว้ภายในคอนเทนเนอร์โดยไม่ติดตั้งซ้ำซ้อนบนเครื่องหลัก เหตุเพราะบนเครื่องเราที่ติดตั้ง VS Code ไม่มี lib ดังกล่าว IntelliSense จึงดูเป็นฟังก์ชันพิการ ยิ่งถ้าติดตั้ง extensions บน VS Code สำหรับ lib เวอร์ชันหนึ่ง แต่ในคอนเทนเนอร์กลับใช้ lib คนละเวอร์ชันด้วยแล้ว extensions นั้นยิ่งดูเหมือนพิการซ้ำซ้อนเข้าไปเลยหละ

ถ้าติดตั้ง lib ไว้บนเครื่องหลัก VS Code จะจัดการ IntelliSent ได้อย่างดีเยี่ยม

VS Code IntelliSent

แต่ถ้า lib มีอยู่เฉพาะใน container VS Code ก็จะดูพิการซ้ำซ้อนนิดหน่อย

VS Code w/o IntelliSent

หาก IntelliSent มันเป็นปัญหามากนัก ย้าย extensions กับการทำงานพวกนี้ไปไว้ในคอนเทนเนอร์ซะเลยซิ! VS Code จะได้มองเห็นว่ามี lib อะไรอยู่ในคอนเทนเนอร์บ้าง extensions จะได้โชว์ IntelliSent ได้อย่างถูกต้อง

VS Code Architecture Containers

จากแผนภาพ VS Code จะมีสองส่วน ส่วนแรกคือส่วน UI ที่ทำงานอยู่บนเครื่องเรา และส่วน Server ที่เสมือนเป็นปรสิตแอบกระดึ๊บไปฝังตัวอยู่ในคอนเทนเนอร์ Server มองเห็น lib อะไร ส่วน UI ก็จะจัดการแสดงผลออกมาได้อย่างถูกต้อง

เพื่อความสงบของมวลมนุษยชาติ VS Code ตอนนี้ได้สนับสนุนความสามารถของ extension ที่เรียกว่า Visual Studio Code Remote - Containers แล้วหละ

เตรียมความพร้อมก่อนใช้ Remote Containers

หมายเหตุ บทความนี้เขียนขึ้นบนฐานของการใช้งาน Docker บน MacOS ผู้ใช้งานระบบปฏิบัติการอื่น สามารถอ่านเพิ่มเติมได้จากลิงค์ท้ายบทความฮะ

  1. เรากำลังพูดถึงคอนเทนเนอร์กันอยู่ใช่ไหม ดังนั้นสิ่งแรกที่พลาดไม่ได้คือติดตั้ง Docker
  2. VS Code ตัวหลักนั้นยังไม่สนับสนุนความสามารถนี้ ต้องติดตั้ง Visual Studio Code Insiders เพื่อทดสอบ
  3. จะเล่นละครอย่าลืมพระเอก! Remote Development extension คือส่วนสำคัญ ติดตั้งลงไปในตัว Insider (อย่าเผลอไปลงในตัวหลักหละ)
  4. ถ้าผ่านมาถึงขั้นตอนนี้ได้ แสดงว่าคุณทำบุญมาเยอะ~

รู้จักกับ devcontainer.json

จะให้ VS Code ทำงานกับ container ได้อย่างถูกต้องก็ต้องแนะนำ Dockerfile ให้ VS Code รู้จักซะหน่อย จะได้อัดฉีดตัว server แฝงเร้นไปเป็นปรสิตได้อย่างถูกต้อง นั่นคือหน้าที่สำคัญของไฟล์ devcontainer.json ที่จะบอกข้อมูลต่าง ๆ ที่ VS Code ต้องการ

เราจะลองส่องโค้ดที่สมบูรณ์แล้วจากการดูดโค้ดด้วยคำสั่ง git clone https://github.com/Microsoft/vscode-remote-try-node ภายหลังการติดตั้งภายใต้โฟลเดอร์ vscode-remote-try-node จะเจอโฟลเดอร์ย่อยชื่อ .devcontainer ที่เก็บไฟล์ devcontainer.json ไว้ หน้าตาประมาณนี้

Code
1{
2 "name": "Python Sample",
3 "dockerFile": "Dockerfile",
4 "appPort": 9000,
5 "context": "..",
6 "extensions": ["ms-python.python"]
7}

เราพบว่าการตั้งค่านี้ชี้ไปที่ Dockerfile ที่เราต้องการใช้งาน

ลำดับถัดไปคือการ build image และเริ่มการทำงานของคอนเทนเนอร์ เริ่มจากกด CMD + Shift + P หรือ จิ้มพรวดไปที่ไอคอนซ้ายล่างพร้อมเลือกเมนู Remote-Containers: Open Folder in Container สำคัญมากคืออย่าลืมติดตั้ง Remote Development Extension ก่อนครับ ไม่งั้นเมนูไม่โผล่นะ

Remote Dev Status Bar

ภายหลังการเลือกเมนูดังกล่าว ให้เราเลือกโฟลเดอร์ของโปรเจคที่เราต้องการทำงานด้วย (ก็คือโปรเจคที่ได้มาจาก git clone นั่นหละฮะ) เสร็จสิ้นแล้วรอมัน build image ซักนิดนึง ขั้นตอนนี้ VS Code Server ก็จะแอบมุดรูหนอนเข้าไปในคอนเทนเนอร์ด้วย

ลำดับสุดท้ายคือการ forward port จากคอนเทนเนอร์ให้บนเครื่องเราสามารถเข้าถึงได้ ถ้าแอบดูใน devcontainer.json จะเห็นว่ามันคือพอร์ต 9000

ตรงส่วนนี้ให้กดไอคอนบนกล่องสีเขียวซ้ายล่างอีกครั้งหรือกด CMD + Shift + P พร้อมเลือก Remote-Containers: Forward Port from Container...

Forward Port

เลือกทำการ forward จาก 9000 บน container มาที่เครื่อง host ได้เลย หากเลือก Open Browser จากกล่องเมนูที่แสดงล่างขวา ก็จะเปิดเว็บไปที่ http://localhost:9000 พร้อมแสดงข้อความอย่างถูกต้อง

Forward Port

ในส่วนของ packages ต่าง ๆ ที่ใช้ในโค้ดของเราจะถูกติดตั้งที่ /usr/local/lib/python3.7/site-packages/ ภายในคอนเทนเนอร์

Packages

เนื่องจาก VS Code Server ได้ถูกติดตั้งในคอนเทนเนอร์แล้ว IntelliSent ก็ควรจะทำงานได้อย่างถูกต้องแม้บนเครื่องหลักของเราจะไม่มีแพคเกจอะไรอยู่เลยก็ตาม ลองเปิดไฟล์ app.py ทดสอบดูได้ฮะ

VS Code IntelliSent

Extensions และ Container

Extensions บน VS Code สามารถติดตั้งได้ทั้งแบบ global และบน workspace (บนโปรเจคที่ทำงานอยู่) ในกรณีของการใช้งานบน workspace เราสามารถระบุชื่อ extensions ในไฟล์ .vscode/extensions.json เพื่อให้ VS Code แจ้งเตือนให้เพื่อน ๆ ในทีมของเราติดตั้ง extensions เหล่านี้สำหรับโปรเจคที่ทำงานร่วมกันได้

การใช้ extensions กับ Remote Container นั้นยิ่งง่ายเข้าไปใหญ่ครับ เพราะเราแค่ระบุ extensions ไว้ในไฟล์ .devcontainer VS Code ก็จะติดตั้ง extensions เหล่านั้นลงไปในคอนเทนเนอร์ให้อัตโนมัติ

Code
1{
2 "name": "Python Sample",
3 "dockerFile": "Dockerfile",
4 "appPort": 9000,
5 "context": "..",
6 "extensions": ["ms-python.python"]
7}

จากตัวอย่างข้างต้น เราพบว่า extension ชื่อ ms-python.python จะถูกติดตั้งลงไปในคอนเทนเนอร์อัตโนมัติ ไม่ได้ติดตั้งบนเครื่อง host ของเรา

Remote Extensions

การใช้งาน Remote Container กับ Docker Compose

Remote Container นั้นสนับสนุนการทำงานกับ Docker Compose เช่นกันครับ เพียงแค่ระบุ dockerComposeFile เพื่อชี้ตำแหน่งของไฟล์ docker-compose.yml ให้ถูกต้องใน .devcontainer.json ก็เป็นอันเสร็จสิ้น

ลองดูตัวอย่างการใช้งานดูด้วยโปรเจคที่มีโครงสร้างดังนี้

Docker Compose Structure

ไฟล์ devcontainer.json ทำการตั้งค่าของ dockerComposeFile ดังนี้ครับ

Code
1{
2 "name": "Node.js",
3 "dockerComposeFile": "../docker-compose.yml",
4 "service": "api",
5 "workspaceFolder": "/app",
6 "shutdownAction": "stopCompose",
7 "extensions": ["dbaeumer.vscode-eslint"]
8}

โค้ดในไฟล์ index.js ไม่มีอะไรมากใช้งาน express และ lib ที่ชื่อว่า greeting เพื่อสุ่มคำทักทายชาวโลก!

JavaScript
1const express = require('express')
2const { random } = require('greeting')
3
4const app = express()
5
6app.get('/', (_, res) => res.send(random()))
7
8app.listen(3000, '0.0.0.0', () => console.log('Listening...'))

แน่นอนว่าเมื่อใช้งาน lib ทั้งสอง ย่อมต้องระบุไว้ใน package.json ด้วยเช่นกัน

Code
1{
2 "name": "api",
3 "version": "1.0.0",
4 "main": "index.js",
5 "license": "MIT",
6 "scripts": {
7 "start": "node src/index.js"
8 },
9 "dependencies": {
10 "express": "^4.16.4",
11 "greeting": "^1.0.6"
12 }
13}

สุดท้ายคือการใช้งาน Docker Compose แบบง่ายด้วยไฟล์ docker-compose.yml

Code
1version: '3'
2services:
3 api:
4 build: ./api
5 volumes:
6 - ./api:/app/
7 - /app/node_modules
8 ports:
9 - '3000:3000'
10 command: yarn start

จากไฟล์ docker-compose.yml ที่ระบุนี้บนเครื่อง host จะไม่มีรายการ packages ต่าง ๆ ในโฟลเดอร์ node_modules อีกต่อไป แต่จะมีเฉพาะในคอนเทนเนอร์เท่านั้น

ก่อนหน้านี้หากเราปิดกั้นไม่ให้บนเครื่อง host ที่ซึ่ง VS Code สถิตอยู่ มีไลบรารี่ต่าง ๆ ในโฟลเดอร์ node_modules การปิดกั้นนี้จะทำให้ VS Code ไม่รู้จัก lib ต่าง ๆ เป็นเหตุให้ IntelliSent ทำงานได้อย่างไม่สมบูรณ์

ครั้นจะแก้ไขปัญหาข้างต้นด้วยการ sync โฟลเดอร์ node_modules ระหว่าง host กับคอนเทนเนอร์ก็จะพบปัญหาที่ว่าถ้าไม่ใช่ Linux การ sync ไฟล์ที่มากเกินไปส่งผลต่อความช้าในการทำงาน แม้จะมีโปรเจคอย่าง docker-sync ช่วยแก้ปัญหานี้ความฟินก็ยังอาจสะดุดได้

การมาของ Remote Container นี้ทำให้เราไม่ต้อง sync node_modules แต่ให้ VS Code Server และ extensions ทำงานกับ node_modules ในคอนเทนเนอร์แทนนั่นเอง

ทดสอบการทำงานของ Docker Compose และ VS Code Remote Container จะพบว่าสามารถทำงานควบคู่กันได้เช่นกัน

ทดลองเปิดไฟล์ index.js จะพบว่าตอนนี้ VS Code ได้ทำ IntelliSent โดยอิงกับ node_modules ในคอนเทนเนอร์เป็นที่เรียบร้อยครับ

Docker Compose IntelliSent

ข้อจำกัดของ Remote Containers

อย่างแรกที่รู้สึกกระอักกระอ่วนคือการที่ฟีเจอร์นี้ยังใช้กับอิมเมจตระกูล Alpine Linux ไม่ได้นี่หละ ก็ต้องรอดูต่อไปว่าตัว official จะใช้งานได้กับ Alpine ไหม ส่วนข้อจำกัดอื่น ๆ อ่านเพิ่มเติมได้จากเอกสารอ้างอิงเลยครับ

เอกสารอ้างอิง

Developing inside a Container. Retrieved May, 4, 2019, from https://code.visualstudio.com/docs/remote/containers

สารบัญ

สารบัญ

  • เตรียมความพร้อมก่อนใช้ Remote Containers
  • รู้จักกับ devcontainer.json
  • Extensions และ Container
  • การใช้งาน Remote Container กับ Docker Compose
  • ข้อจำกัดของ Remote Containers
  • เอกสารอ้างอิง