Deploy Node.js ด้วย Azure App Service Plan, Azure Container Registry และ GitHub Action

Ponggun
T. T. Software Solution
9 min readOct 8, 2023

สวัสดีผู้อ่านทุกท่านนะครับ ก่อนหน้านี้ผมมีโอกาสได้สรุปแนวทางการ Deploy .NET Web App ขึ้นไปที่ Azure App Service และสามารถสั่งให้เกิดการ Deploy ได้แบบอัฒโนมัติถ้ามีการแก้ไขเกิดขึ้นที่ GitHub ด้วยความช่วยเหลือของ GitHub Action ครับ

ท่านใดที่สนใจสามารถศึกษาวิธีได้ที่ Video นี้นะครับ หลักๆอยากให้ดูตรงนี้ก่อนเพื่อเข้าใจคำศัพท์ต่างๆที่เกี่ยวข้องกับการใช้งาน Azure App Service ครับผม

ในระหว่างการสอน มีคำถามนึงที่น่าสนใจมาก ว่านอกจากจะ Deploy .NET Web App แล้ว อยากขอให้ทดลอง Deploy Node.js ด้วยและอยากให้ Deploy แบบ Container ครับ ในบทความนี้เลยจะมาพาผู้อ่านทดลองกันนะครับ ลุยยย

สิ่งที่ต้องเตรียมทั้งหมด

  1. Docker Desktop เพื่อใช้รัน Docker Cli นะฮะ
  2. GitHub Repository ที่เก็บ Node.js Web App ของเรา รวมไปถึง Dockerfile เพื่อสร้าง Docker Image นะครับ และเราจะสร้าง GitHub Action Pipeline ใน Repository นี้เช่นกันครับ
  3. Azure Container Registry เพื่อใช้ในการเก็บ Docker Image ของเราและนำไป Deploy ต่อที่ Azure App Service ครับ
  4. Azure App Service Linux เป็นที่ที่เราจะนำ Docker Image มา Deploy เพื่อเป็น Web App หลักของระบบนะครับ

แอบสปอยว่ามีตัวช่วยสร้าง Pipeline ให้อัตโนมัติเลยครับ รวมถึง Secret Key ต่างๆที่ต้องใช้ในการเชื่อม GitHub และ Azure ด้วยครับ สะดวกสุดๆไปเลยพี่น้อง

เส้นทางที่เราจะเดินไปให้ถึงในบทความนี้นะครับ : )

Docker Desktop

ใครที่ยังไม่เคยติดตั้ง Docker Desktop รวมถึงการใช้คำสั่งเบื้องต้นต่างๆ ผมอยากขอแนะนำลองทำตามบทความนี้ได้นะครับสำหรับสาย Windows โดยใช้ร่วมกับ WSL2 ครับผม

ส่วนถ้าใครใช้ Mac ก็ลองตามนี้นะครับ

GitHub Repository

ให้เราทำการ Fork Repository ของผมได้เลยนะครับ เป็นตัวอย่าง React App ง่ายๆที่เตรียม Dockerfile มาพร้อมเลย มาลองรันในเครื่องกัน

เมื่อได้ Code ในเครื่องแล้วจะเห็นหน้าตาประมาณนี้นะครับ โดยเป็น Project ที่สร้างมาจาก Create React App แบบเริ่มต้นเลยไม่ได้ปรับแต่งอะไรครับผม

ฮิฮิ Dockerfile, docker-compose.yaml มาพร้อมแล้ว ว้าวุ่นเลยทีนี้

เรามาลองรัน Web App กันง่ายๆก่อนด้วยคำสั่ง

npm start

เสร็จแล้ว Click Link http://localhost:3000/ เพื่อดูผลนะครับ

มาแล้ววว

ทีนี้เรามาลองสร้าง Docker Image กันนะครับ เนื่องจากเรามี Docker File อยู่แล้ว ให้ลองรันคำสั่งข้างล่างเพื่อสร้าง Docker Image ที่ชื่อ react-azure กันนะครับ

docker build -t react-azure .

เสร็จแล้วให้ลองเปิด Docker Desktop จะพบว่ามี Docker Image ที่เราต้องการขึ้นมาแล้วนะครับ

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

ถ้าท่านใดอยากทราบวิธีการสร้าง Docker File โดยละเอียด สามารถติดตามได้จากบทความนี้นะครับ ปูพื้นฐาน Docker, Docker Compose แน่นๆเยย เย้

ให้ลองรันคำสั่งข้างล่างนี้เพื่อรัน Web App ของเราอีกครั้ง แต่เปลี่ยนจากการรันด้วย Node Runtime มาเป็น Docker Runtime แทนนะครับ เพื่อทดสอบว่า Docker Image ของเราทำงานได้ถูกต้องจริงๆ

docker run -p 8000:80 --name react-azure react-azure

เหตุผลที่ต้อง Map Docker Port 80 เพราะว่าใน Dockerfile เราใช้วิธี Build แบบ Multi Stage ครับผม เพื่อลดขนาดของ Docker ลง โดยนำ Nginx มาช่วย และมีการ Configure Port 80 ไว้ครับ

Nginx Configure Port 80
Docker Build Multi-stage with Nginx

เมื่อรันเสร็จแล้วให้เราเปิด Docker Desktop อีกทีนะครับ จะพบว่ามี Container ขึ้นมาแว้ว แปลว่า Web App ของเราพร้อมใช้งานแล้วครับ

เราสามารถ Click ที่ `Port(s)` เพื่อเปิด Web App ได้เลยครับ สะดวกมาก

ที่เด็ดที่ยก Docker Desktop ขึ้นมาคือ การดู log ที่สะดวกมากๆ, ตรวจเช็คย้อนหลังได้ว่าเรา Mount Port, Environment อะไรไว้บ้าง รวมไปถึงการใช้ Terminal ใน Docker Container ของเราอีกที เผื่ออยากเข้าไปตรวจสอบว่า Docker ของเราทำงานได้สมบูรณ์ไหมนะครับ

มีตัวช่วยมันดีแบบนี้นี้เองงงงง ^^/

ในเมื่อเรามี docker-compose.yaml ให้ใช้งานได้สะดวกๆแล้ว เลยอยากแนะนำวิธีรัน Web App อีกแบบที่ Code สั้นกว่าเยอะเลยฮะ ไม่ต้องใส่ Options เยอะๆแล้ว

ก่อนรันให้ปิด Docker Container ก่อนหน้านี้ด้วยนะครับ

docker compose up

กลับไปดูผลลัพธ์ที่ Docker Desktop กันอีกทีนะครับ ก็จะพบว่า Web App เราพร้อมใช้ในอีกรูปแบบนึงแล้วครับผม เท่านี้การเตรียมการ Docker Image

Docker Container ที่ถูกรันด้วย Docker Compose มาแล้วครับ ลองเลือกวิธีที่สะดวกกันได้เลยน้า
จะเห็นว่าได้ Images คนละอันกันนะครับ ซึ่งผมตั้งใจให้เป็นแบบนั้น เพราะถ้าเวลาสั่งรัน Docker Compose จะได้ไม่ต้องไปสร้าง Image ก่อน แต่ให้สร้าง Image พร้อมรัน ให้เลยในจังหวะเดียวเพื่อความสะดวกครับผม

ปัญหาเรื่อง Platform ที่เจอเฉพาะบน Mac M1/M2

เนื่องจากในช่วงที่ผมทดลองนำ Docker Image ที่ Build จากเครื่องของผม ที่เป็น Macbook Air M2 แล้วพบปัญหาว่า Image บางตัวรันบน Azure ไม่ได้ครับผม

เพื่อแก้ไขปัญหาในจุดนี้ ผมจึงทำการ Build Image โดยระบุ Platform ไปเลยว่าสำหรับ Linux ครับผม ซึ่งมีข้อดีคือ Image นี้ไม่มีปัญหาใน Azure ละ แต่รันในเครื่องผมไม่ได้นะ ฮ่าๆ เลยจะเป็นเศร้าๆนิดนึง เวลาอยากทดลองในเครื่องก็ Build Image นึง เวลาอยากเอาขึ้น Azure ก็ Build อีก Image นึงแทนชั่วคราวไปก่อนนะครับ

กราบขอโทษที่ยังหาวิธีที่ดีกว่านี้ไม่ได้ครับ T__T

แอบแนะนำบทความคุณสมเกียรติครับผม สุดยอด

ขั้นตอนการ Build จะเปลี่ยนเป็นใช้คำสั่งดังนี้นะครับ

docker buildx build --platform linux/amd64 -t {image} .

เมื่อ Build ได้แล้วก็มาลองดูผลลัพธ์กันจะเห็นว่าขึ้นแดงเลยว่าเป็น AMD64 ไม่ใช่ Arm แหล่ว

เผื่อท่านใดอยากจะ Build Docker แบบ Multi-Platforms ลองคำสั่งตามนี้ได้นะครับ (เครื่องผมรันไม่ได้ ฮะๆ ได้ทีละ Platform)

docker buildx build --platform linux/arm64,darwin/amd64 -t {image} .

Azure Container Registry (ACR)

เป็นพื้นที่เก็บ Docker Images คล้ายๆ Docker Hub นะครับ แต่จะเป็นบริการบน Microsoft Azure ถ้าใครสนใจศึกษารายละเอียดเพิ่มเติมสามารถดูได้ที่นี้ครับ

เพื่อความสะดวกผมขอย่อคำว่า Azure Container Registry เหลือแค่ ACR นะครับ

ในขั้นตอนการสร้าง ACR ให้เราทำขั้นตอนข้างล่างเพื่อ Pin Menu ให้เข้าถึงได้สะดวกๆก่อนนะครับ

เสร็จแล้วให้กดที่ Menu Container registries ทางซ้ายมือแล้วกด Create เพื่อสร้าง ACR ได้เลยครับ

ใน Tab Basics ให้เราทำการระบุชื่อ Registry/Location ที่ต้องการ และเลือก Pricing Plan Basic ที่จะมีค่าใช้บริการต่อเดือนประมาณ 7 USD ครับ (ถ้าท่านใดต้องการเลือก Package อื่น สามารถศึกษาได้ที่นี้นะครับ Pricing — Container Registry)

ส่วนข้อมูลอื่นให้คงไว้ตาค่าเริ่มต้นแล้วกดสร้างได้เลยครับ

เมื่อสร้างเสร็จแล้วให้เราทำการเพิ่ม Admin user เพื่อใช้ในการ Login เพื่อ Push Docker Image จากเครื่องเราเข้าไปเก็บที่ ACR นะครับ

Microsoft แนะนำให้ใช้ Admin user เฉพาะกรณีของการทดสอบเบื้องต้นเท่านั้นนะครับ แนวทางที่ปลอดภัยกว่าคือการใช้ Tokens เพื่อกำหนดสิทธิได้แยกตามการใช้งานนะครับ

เมื่อได้ User พร้อมแล้ว ให้เรากลับมาที่ Terminal ของเราและทำการ Login ด้วย Docker Login CLI นะครับ ระบุ User / Password ตาม Access Key ข้างบนได้เยย

docker login {ACR Path}

ตัวอย่าง
docker login ttssreactazure.azurecr.io

เสร็จแล้วให้ทำการ Tag Docker Image เพื่อเตรียมที่จะ Push ขึ้น ACR ครับผม

docker tag {image} {ACR Path}/{image}

ตัวอย่าง
docker tag react-azure ttssreactazure.azurecr.io/react-azure
docker tag react-azure-amd ttssreactazure.azurecr.io/react-azure-amd

ขั้นตอนสุดท้ายแล้วครับ สั่ง Push ขึ้น ACR ได้เลย

docker push {ACR Path}/{image}

ตัวอย่าง
docker push ttssreactazure.azurecr.io/react-azure
docker push ttssreactazure.azurecr.io/react-azure-amd

ดูผลลัพธ์ใน ACR จะพบว่ามี Docker Image ใน Repository มาแล้วนะครับ

อ้าววว ทำไม 401 ฮ่าๆๆๆ ว้าวุ่นเลยทีนี้

ถ้าใครเจอปัญหาเหมือนผมนะครับคือไม่สามารถดู Repository ได้ ให้ทำการเพิ่ม Reader Role ใน IAM นะครับ จบปิ้งงงง

กลับไปดู Repository คราวนี้มาแน่นอนครับ

Image ก็พร้อมบน ACR แล้ว เดี๋ยวเราไปลองติดตั้งบน Azure App Service Plan กันฮะ

Azure App Service Plan Linux — Container

เป็นบริการ Web Server แบบ PaaS ที่ผมชื่นชอบมานานครับ เพราะตอบโจทย์การทำงานหลายรูปแบบ เราเลือกได้ทั้ง Windows / Linux รวมไปถึง Runtime ต่างๆทั้งแบบ Code ที่มี Runtime ติดตั้งมาให้ และแบบ Docker ที่เราเอา Image ไปวางได้เลย แล้วรันแบบ Container ครับผม ถ้าใครสนใจรายละเอียดเพิ่มเติม ผมแนะนำ Video ข้างบนน้า…

ตอบโจทยการใช้งานที่หลากหลายมากครับ เอาอยู่ตั้งแต่ Legacy ยัน Modern Web App เลย

เรามาเริ่มสร้าง App Service Plan กัน โดยเริ่มจากการเพิ่มเมนูให้อยู่ทางซ้ายมือก่อนเพื่อสะดวกในการค้นหานะครับ

มีสองเมนูนะครับ App Service Plan สำหรับดูแล Web Server, App Services สำหรับดูแล Web Instance ใน Web Server อีกที

ให้เรากด Menu App Service plans เพื่อทำการสร้าง Web Server Linux กันตามนี้นะครับ เน้นที่

  • Operating System = Linux เพราะเราต้องใช้ Docker Container Linux
  • Pricing Plan = Basic จะได้ประหยัดค่าใช้จ่ายครับ (ราคา 14.60 USD ต่อเดือน)

ส่วนข้อมูลอื่นๆใช้เป็นค่าเริ่มต้นได้หมดเลยครับ

กด Review + Create เพื่อสร้าง App Service Plan Linux -> จบปิ๊ง

ขั้นตอนถัดมาให้เรากด Menu App Services เพื่อทำการสร้าง Web App กันซะทีนะครับ ฮ่า ลากยาวมาหลายขั้นตอน โดยเน้นที่

  • Publish = Docker Container
  • Operating System = Linux
  • Region = เลือกให้ตรงกับที่เราสร้าง App Service Plan
  • Linux Plan = เลือก App Service Plan ที่เราพึ่งสร้างได้เลยครับ

ในขั้นตอนถัดมานี้สำคัญเลยครับ คือการเลือก Docker Image จาก ACR ของเราเพื่อติดตั้งลงใน Web นี้นะคร้าบ

ข้อมูลอื่นๆให้คงไว้ที่ค่าเริ่มต้นนะครับและกด Create ได้เลย เราจะได้หน้าตาประมาณรนี้เมื่อสร้างเสร็จแล้วครับ

ให้ลองกดจิ้มที่ Default domain เราจะพบกับหน้าตา Web ที่ค้างไม่มาสักทีครับฮ่าๆๆๆ มีอะไรที่ทำทีเดียวผ่านไหมนะ

หยอกๆๆๆๆ จริงๆตัวระบบกำลัง Pull Image ลงมาครับ เลยต้องให้เวลาพี่แกหน่อย ยิ่ง Image ใหญ่ก็ยิ่งใช้เวลานานครับ ต้องไปปรับขนาดให้ Image ลดลงในจุดที่เหมาะสมน้า

รอเฮียแก Pull Image นิสสสส

หลังจากรอคอยสักพัก ในที่สุดดดด

T_T มาศะทีน้ำตาจิไหล

ถ้าใครอยากดู Log แล้วรู็สึกว่าใน Deployment Center มันดูไม่สะใจ ผมแนะนำให้เปิด Application Log ตามขั้นตอนนี้นะครับ

ขั้นตอนแรกให้ไปที่ App Service logs -> กด File System -> เลือก 1 Day Retention Period

เสร็จแล้วเลือก Advanced Tools -> Go -> Log stream

คราวนี้ดูผ่าน Web เบิ้มๆไปเลย อยาก Reload Log ก็กด Refresh รัวๆเท่าที่ใจเธอต้องการ (เผื่อรอ Auto Reload แล้วไม่ทันใจ)

GitHub Action

ก่อนหน้านี้เราได้ทดลองติดตั้ง Docker Image ลงบน Azure App Service ด้วยมือกันแล้วนะครับ ซึ่งในการทำงานจริงๆเราต้องพยายามปรับปรุงการใช้เวลาของเราให้เหมาะสม เพราะงั้นการทำให้ขั้นตอนการติดตั้งทำได้อัตโนมัติจึงเป็นเรื่องที่จำเป็นสำหรับเรามากๆเลยครับผมเพื่อให้มั่นใจว่าเราสามารถส่งมอบงานถึงมือลูกค้าได้ไวเพื่อให้ได้ทดลองใช้งานและนำ Feedback มาปรับปรุงระบบให้ดียิ่งขึ้นนะครับ

ในขั้นตอนสุดท้ายนี้จะเป็นการ Setup ให้ระบบทำการ Auto Deploy ให้เราเลยครับผม โดยจะมีขั้นตอนตามภาพนี้นะครับ

ขั้นตอนแรกให้เรากลับไปที่ Azure App Service ของเรานะครับ เลือก Deployment Center -> Setting -> เลือก GitHub Action เพื่อเปลี่ยนการ Deployment ของเราให้เป็นแบบอัตโนมัติด้วย GitHub Action แทนครับผม

ในส่วนของ GitHub Actions ให้เราระบุ Repository ที่เราเก็บ Code ที่ต้องการสั่ง Deployment นะครับ รวมถึงเลือก Branch ของเราด้วยว่าถ้า Commit ไปที่ Branch ไหนแล้วจะเริ่มต้นสั่ง Deployment ครับ ในที่นี้ก็ Main เยย

ในส่วนของ Registry settings ก็คงค่า Default ที่เราเลือกไปครับ จะเป็นการระบุว่าเวลาที่มีการ Deployment เกิดขึ้นนั้น จะมีการสร้าง Docker Image และ Push ไปที่ ACR ที่เราระบุครับผม ส่วน Tag นั้น GitHub ทำให้อัตโนมัติคร้าบบบ

เสร็จแล้วให้เราลองกด Preview ดู จะพบว่า Azure จะสร้าง GitHub Action Pipeline ให้ดังนี้นะครับ (ขั้นตอนคร่าวๆก็ตามภาพข้างบนเลย)

# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy container app to Azure Web App - ttssreactazureamd

on:
push:
branches:
- main
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Log in to container registry
uses: docker/login-action@v2
with:
registry: https://ttssreactazure.azurecr.io/
username: ${{ secrets.AzureAppService_ContainerUsername_69666a375480421e9cb40acacdd9e947 }}
password: ${{ secrets.AzureAppService_ContainerPassword_44596b20cc7643358e82d9d3fbf29a37 }}

- name: Build and push container image to registry
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ttssreactazure.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_69666a375480421e9cb40acacdd9e947 }}/react-azure-amd:${{ github.sha }}
file: ./Dockerfile

deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'production'
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

steps:
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v2
with:
app-name: 'ttssreactazureamd'
slot-name: 'production'
publish-profile: ${{ secrets.AzureAppService_PublishProfile_b57e6e4b9ffe4583912aba8d443edb8d }}
images: 'ttssreactazure.azurecr.io/${{ secrets.AzureAppService_ContainerUsername_69666a375480421e9cb40acacdd9e947 }}/react-azure-amd:${{ github.sha }}'

เมื่อเรากด Save ระบบก็จะเริ่ม Build Pipeline ให้ในครั้งแรกเลยนะครับ เราสามารถกดที่ Build Logs เพื่อไปยัง GitHub Action ต่อ

Build ให้อัตโนมัติเลยที่ GitHub Action ครับ สะดวกจัง T_T

ทีนี้ อยากให้เราลองไปแอบส่องนิดนึงว่าทำไม GitHub Action มันถึงเชื่อม Azure App Service และ ACR ของเราได้นะครับ

สาเหตุหลักๆก็เป็นเพราะว่า Azure ช่วยสร้าง Secret ไว้ใน GitHub Action และทำการใช้งานใน GitHub Pipeline เลยครับผม ชีวิตสะดวกจัดๆแล้วจังหวะนี้

มี GitHub Pipeline ให้ดูที่นี้นะครับ ทำให้เราสามารถมาแก้ต่อทีหลังได้สบายเลย

https://github.com/T-T-Software-Solution/react-in-azure-appservice-linux-container/blob/main/.github/workflows/main_ttssreactazureamd.yml

ถ้าย้อนมาดูที่ App Service เราจะพบ Log อย่างสวยงามครับ

เราจะเห็นว่ามี Imageใหม่ถูก Deploy เข้า ACR และถูกนำมารันที่ App Service แล้วนะครับ

การบ้านครับ

เผื่อท่านใดอยากลอง Image อื่นๆนอกเหนือจาก Node.js/React สามารถทดลองตาม Repository ข้างล่างนี้ได้เลยนะครับ

สำหรับ Docker Image ที่ไม่ได้ใช้ Port 80 ผมแนะนำให้เพิ่มข้อมูลใน Azure Configuration ดังนี้นะครับ

  • WEBSITES_PORT ในการ Bind Docker Port เช่น WEBSITES_PORT = 3000 จะหมายถึง -p 80:3000
  • WEBSITES_CONTAINER_START_TIME_LIMIT เป็นเวลาที่ใช้รัน Docker ใน Azure ควร Set ไว้มากสุดที่ 1800 วินาที

.NET and Node.js

Python

Spring Boot

สรุป

ขอบคุณผู้อ่านทุกท่านที่มาถึงหัวข้อสรุปสุดท้ายนะครับ ผมคาดหวังว่าอย่างน้อยๆผู้อ่านจะมองเห็นภาพและแนวทางการทำงานในการติดตั้ง Docker Image ลงบน Azure App Service และสามารถทำงานได้อย่างต่อเนื่องและรวดเร็วในการติดตั้งครั้งถัดๆไป ด้วยความเชื่อมโยงของ App Service, ACR และ GitHub Action ในการสร้าง Pipeline เพื่อ Continous Deployment นะครับ

ขอให้มีความสุขกับการเขียน Code นะครับ

นายป้องกัน : )

--

Ponggun
T. T. Software Solution

Development Manager, Web Developer with ASP.Net, ASP.net Core, Azure and Microsoft Technologies