CodeDeploy: Deploy In-Place Đầu Tiên Lên EC2

K
Kai··6 min read

Code đã build và test. Chặng cuối của dây chuyền là đưa nó lên server đang chạy — và đây là phần dài nhất, quan trọng nhất của series. Part IV dành cho CodeDeploy, nhắm vào EC2. Bài này dựng nền: một EC2 instance có agent, một application và deployment group, một appspec.yml mô tả cách deploy, rồi chạy lần in-place deploy đầu tiên và quan sát từng bước.

💰 Chi phí

Bài này (và cả Part IV) chạy một EC2 t3.micro. Nó rẻ (khoảng 0,01 USD/giờ) và thuộc free-tier ở nhiều tài khoản, nhưng tính tiền khi chạy — nhớ terminate khi học xong Part IV (mục dọn dẹp ở cuối). CodeDeploy bản thân nó miễn phí cho deployment lên EC2.

Mục tiêu

Hiểu các thành phần của CodeDeploy và cách chúng khớp nhau, rồi chạy một in-place deploy thật lên EC2 và đọc được lifecycle event.

Các mảnh của CodeDeploy

CodeDeploy có vài khái niệm cần phân biệt. Application là vùng chứa logic cho một ứng dụng. Deployment group là tập máy đích cùng cách deploy (nhắm máy theo tag hoặc theo Auto Scaling group). Revision là gói nội dung cần deploy (bundle zip có appspec.yml ở gốc). Agent là tiến trình chạy trên mỗi EC2, liên tục hỏi CodeDeploy "có gì để deploy không". appspec.yml là file mô tả copy gì đi đâu và chạy script nào ở mỗi giai đoạn.

Service role và EC2 cần gì

CodeDeploy cần một service role để thao tác thay bạn. Trust policy cho codedeploy.amazonaws.com, và với EC2 thì đính managed policy AWSCodeDeployRole:

$ aws iam create-role --role-name awscicd-codedeploy-role \
    --assume-role-policy-document file://cd-trust.json
$ aws iam attach-role-policy --role-name awscicd-codedeploy-role \
    --policy-arn arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole

Còn bản thân EC2 cần hai thứ: một instance role để agent đọc được revision từ S3, và agent được cài. Instance role (qua instance profile) cấp quyền s3:Get* trên bucket artifact; agent cài qua user-data lúc khởi động:

#!/bin/bash
dnf install -y ruby wget httpd
systemctl enable --now httpd
cd /tmp
wget -q https://aws-codedeploy-ap-southeast-1.s3.ap-southeast-1.amazonaws.com/latest/install
chmod +x ./install && ./install auto
systemctl enable --now codedeploy-agent

Launch instance với instance profile, user-data trên, và tag để deployment group nhắm tới — ở đây App=awscicd-demo:

$ aws ec2 run-instances --image-id <al2023-ami> --instance-type t3.micro \
    --iam-instance-profile Name=awscicd-ec2-profile \
    --subnet-id <subnet> --security-group-ids <sg-http> --associate-public-ip-address \
    --user-data file://userdata.sh \
    --tag-specifications 'ResourceType=instance,Tags=[{Key=App,Value=awscicd-demo}]'

Tag App=awscicd-demo chính là sợi dây nối instance với deployment group: CodeDeploy tìm máy đích bằng tag, không bằng id, nên thêm/bớt máy có cùng tag là tự động vào nhóm.

appspec.yml: kịch bản deploy

File này đặt ở gốc revision, mô tả copy file nào đi đâu và chạy hook nào ở mỗi giai đoạn:

version: 0.0
os: linux
files:
  - source: index.html
    destination: /var/www/html/
hooks:
  ApplicationStop:
    - location: scripts/stop_server.sh
      runas: root
  BeforeInstall:
    - location: scripts/before_install.sh
      runas: root
  ApplicationStart:
    - location: scripts/start_server.sh
      runas: root
  ValidateService:
    - location: scripts/validate.sh
      runas: root

Khối files copy index.html vào thư mục web; các hooks chạy script ở đúng giai đoạn (dừng app cũ, cài gói cần, khởi động, kiểm tra). Script validate.sh curl thử localhost để chắc app sống — nếu nó trả lỗi, deploy coi như fail. Các hook này là chủ đề mổ sâu của bài 9; ở đây chỉ cần một bộ tối thiểu chạy được.

Application, deployment group, revision

Tạo application và deployment group (nhắm tag, kiểu in-place, mỗi lần một máy):

$ aws deploy create-application --application-name awscicd-demo --compute-platform Server

$ aws deploy create-deployment-group --application-name awscicd-demo \
    --deployment-group-name awscicd-demo-dg \
    --service-role-arn arn:aws:iam::111122223333:role/awscicd-codedeploy-role \
    --ec2-tag-filters Key=App,Value=awscicd-demo,Type=KEY_AND_VALUE \
    --deployment-config-name CodeDeployDefault.OneAtATime

Đóng gói revision và đẩy lên S3 bằng aws deploy push (nó zip thư mục hiện tại, upload, và in sẵn lệnh deploy):

$ aws deploy push --application-name awscicd-demo \
    --s3-location s3://awscicd-artifacts-111122223333-ap-southeast-1/revisions/awscicd-demo.zip \
    --source .
To deploy with this revision, run:
aws deploy create-deployment ... key=revisions/awscicd-demo.zip,bundleType=zip ...

Chạy deploy

$ DID=$(aws deploy create-deployment --application-name awscicd-demo \
    --deployment-group-name awscicd-demo-dg \
    --s3-location bucket=awscicd-artifacts-...,key=revisions/awscicd-demo.zip,bundleType=zip \
    --query 'deploymentId' --output text)

$ aws deploy get-deployment --deployment-id $DID --query 'deploymentInfo.status'
InProgress
...
Succeeded

Bên trong một lần deploy

Đây là phần đáng mổ. Khi bạn create-deployment, CodeDeploy không đẩy gì xuống máy. Nó chỉ đánh dấu có job mới cho deployment group. Agent trên EC2 — vốn liên tục poll — thấy job, rồi tự kéo revision từ S3 và chạy qua chuỗi lifecycle event. Xem chuỗi đó trên instance:

$ aws deploy get-deployment-instance --deployment-id $DID --instance-id i-03a4... \
    --query 'instanceSummary.lifecycleEvents[].[lifecycleEventName,status]' --output text
ApplicationStop   Succeeded
DownloadBundle    Succeeded
BeforeInstall     Succeeded
Install           Succeeded
AfterInstall      Succeeded
ApplicationStart  Succeeded
ValidateService   Succeeded

Đọc chuỗi này: ApplicationStop chạy hook dừng app cũ, DownloadBundle agent kéo zip từ S3, BeforeInstall hook chuẩn bị, Install copy file theo khối files của appspec, AfterInstall/ApplicationStart khởi động lại, ValidateService chạy hook kiểm tra. DownloadBundleInstall là sự kiện CodeDeploy tự làm; các sự kiện còn lại chạy hook của bạn. Vì agent kéo (pull) chứ CodeDeploy không đẩy (push), instance không cần mở cổng inbound nào cho việc deploy — đó cũng là lý do mô hình này an toàn với máy nằm sâu trong private subnet.

   create-deployment (revision = zip trên S3)
        │  CodeDeploy đánh dấu job cho deployment group (tag App=awscicd-demo)
        ▼
   ┌──────────── EC2 (agent đang poll) ─────────────┐
   │  agent thấy job → DownloadBundle (kéo zip S3)   │
   │  chạy appspec theo lifecycle event:             │
   │   ApplicationStop → BeforeInstall → Install     │
   │   → AfterInstall → ApplicationStart → Validate  │
   └───────────────────────┬─────────────────────────┘
        index.html → /var/www/html ; httpd phục vụ
                            ▼
        curl http://<ip>/ → "Hello from the awscicd demo app — v2"

Kiểm chứng app phục vụ thật:

$ curl http://<public-ip>/
... <h1>Hello from the awscicd demo app — v2</h1> ...

App đã chạy trên EC2, đưa lên bằng CodeDeploy.

🧹 Dọn dẹp

Bài 9 dùng lại đúng instance và deployment group này, nên giữ tới hết bài 9. Khi dọn (cuối Part IV):

$ aws ec2 terminate-instances --instance-ids i-03a4...
$ aws deploy delete-deployment-group --application-name awscicd-demo --deployment-group-name awscicd-demo-dg
$ aws deploy delete-application --application-name awscicd-demo
$ aws ec2 delete-security-group --group-id <sg>
# instance profile/role giữ tới cuối series

EC2 tính tiền khi chạy, nên đừng quên terminate — đây là tài nguyên tốn tiền chính của series.

Tổng kết

CodeDeploy đưa revision (bundle có appspec.yml) lên các máy trong deployment group, nhắm theo tag. EC2 cần instance role (đọc S3) và agent (cài qua user-data). appspec.yml khai files để copy và hooks để chạy script ở từng lifecycle event. Cơ chế cốt lõi: agent kéo revision khi thấy job, rồi chạy chuỗi ApplicationStop → DownloadBundle → BeforeInstall → Install → AfterInstall → ApplicationStart → ValidateService — instance không cần cổng inbound nào cho deploy. Lần in-place deploy đưa app lên đúng instance đang chạy.

Bài tới mổ sâu phần hook: thứ tự chạy chính xác, từng hook hợp với việc gì, biến môi trường CodeDeploy truyền vào script, và chuyện gì xảy ra khi một hook fail.