1. ภาพรวมการทำงาน
เริ่มจากเลือกแหล่งรับไฟล์ แล้วใช้ขั้นตอนตรวจและจัดเก็บชุดเดียวกัน
| หัวข้อ | LINE Group | Gmail |
|---|---|---|
| วิธีรับไฟล์ | Webhook จาก LINE | Polling ผ่าน Gmail API |
| ความเร็วโดยทั่วไป | ส่งเข้าระบบทันที | ภายในประมาณ 60 วินาที |
| ป้องกันไฟล์เก่า | line_not_before_ms |
gmail_not_before_ms |
| ป้องกันไฟล์ซ้ำ | webhookEventId และ messageId |
messageId และ partId (ไม่ใช้ attachment retrieval ID) |
| ชื่อซ้ำ | คงชื่อเดิม invoice.pdf ทุกไฟล์ โดย S3 ใช้ UUID ป้องกันการทับกัน |
|
1.1 เตรียมความพร้อมก่อน
รายการกลางที่ควรพร้อมก่อนเริ่ม Infrastructure และตั้งค่า Source โดยสรุปจากขั้นตอนติดตั้งทั้งหมดในคู่มือนี้
Domain / Cloudflare / ผู้ดูแลระบบ
1.2 สิ่งที่ต้องเตรียมสำหรับ LINE Group
บัญชีและกลุ่ม
ระบบปลายทาง
1.3 สิ่งที่ต้องเตรียมสำหรับ Gmail
ระบบปลายทาง
3. ตั้งค่าแหล่งรับไฟล์
เลือก LINE Group หรือ Gmail แล้วทำตามลำดับจากบนลงล่าง
docker compose และ IAM Role ของ EC2 โดยไม่ใช้ --profile AWS และไม่ mount ~/.aws. ถ้าทดสอบบน Mac/local ให้ใช้ compose override ของ local แยกต่างหาก.สร้าง LINE Official Account และเปิด Messaging API
เข้า LINE Official Account Manager สร้างหรือเลือกบัญชี จากนั้นเปิดใช้ Messaging API และผูกกับ Provider ที่องค์กรควบคุม
อนุญาตให้ Bot เข้ากลุ่ม
เปิด Messaging API tab แล้วเปิด Allow bot to join group chats จากนั้นเชิญ LINE OA
เข้ากลุ่มเป้าหมาย กลุ่มหนึ่งมี LINE OA ได้ครั้งละหนึ่งบัญชี
ออก Channel access token
เก็บ Channel secret และออก Channel access token จาก channel เดียวกัน Token ใช้ดาวน์โหลดไฟล์ ส่วน secret ใช้ตรวจว่า webhook มาจาก LINE จริง
ระบบปัจจุบันไม่ต่ออายุ token ให้อัตโนมัติ หากเลือก token ที่มีวันหมดอายุ ต้องกำหนดผู้รับผิดชอบและรอบเปลี่ยนค่าใน AWS Secrets Manager ก่อนหมดอายุ หากใช้ long-lived token ให้กำหนดรอบ rotation และ revoke ทันทีเมื่อสงสัยว่ารั่ว
สร้าง LINE secret ใน AWS
สร้าง secret ชื่อ ใน region
{
"LINE_CHANNEL_SECRET": "<channel-secret>",
"LINE_CHANNEL_ACCESS_TOKEN": "<channel-access-token>"
}
aws secretsmanager describe-secret \
--secret-id \
--region
เตรียม public HTTPS webhook
สร้าง DNS ให้ชี้มายัง load balancer/EC2 และใช้ certificate ที่เชื่อถือได้ URL ของระบบคือ:
https:///v1/line/webhook
Endpoint นี้ไม่ใช้ JWT เพราะตรวจ X-Line-Signature จาก raw request body
หา destination และ groupId อย่างปลอดภัย
destination คือ Bot user ID หาได้จาก Get bot info ของ LINE ส่วน groupId
ไม่มี API สำหรับ list กลุ่มทั้งหมด ต้องอ่านจาก webhook หลังเชิญ Bot เข้ากลุ่ม โดย
destination อยู่ระดับบนสุด และ groupId อยู่ที่
events[].source.groupId
{
"destination": "Uxxxxxxxx...",
"events": [{
"source": {
"type": "group",
"groupId": "Cxxxxxxxx..."
}
}]
}
/v1/line/webhook รับเฉพาะ Source ที่ provision แล้ว จึงใช้ค้นหา groupId ครั้งแรกไม่ได้
ให้ใช้ signed webhook receiver ภายในองค์กรที่ตรวจ X-Line-Signature ด้วย Channel secret ก่อนแสดงค่า
หากองค์กรยังไม่มี receiver นี้ให้หยุดขั้นตอนและให้ทีมพัฒนาเพิ่ม discovery mode ที่ใช้ครั้งเดียวก่อน ห้ามใช้
public webhook inspector และห้ามใช้ userId แทน groupIdปิด webhook ชั่วคราวและ provision กลุ่ม
ปิด Use webhook ก่อน แล้วใช้ destination และ groupId ที่ตรวจสอบแล้ว:
docker compose exec -T api \
python manage.py provision_source \
--customer coreth \
--type LINE_GROUP \
--external-ref <destination>:<groupId> \
--channel-destination <destination> \
--secret-ref aws-secretsmanager:// \
--access-token-ref aws-secretsmanager:// \
--line-start-from-now \
--name "Coreth validation LINE group"
source_id=<uuid> และมีค่า
line_not_before_ms
ตรวจ Source และ cutoff
docker compose exec -T api \
python manage.py shell -c "from ingestion.models import Source; s=Source.objects.get(id='<source_id>'); print({'type': s.source_type, 'policy': s.policy, 'enabled': s.enabled})"
ตั้งค่า webhook ใน LINE Developers Console
- วาง Webhook URL
- กด
Verifyให้ขึ้น Success - เปิด
Use webhook - เปิด
Webhook redelivery - เปิด
Error statistics aggregation
ถ้า LINE ส่ง event เดิมซ้ำ ระบบจะใช้ webhookEventId/messageId กันไฟล์ซ้ำ
ส่งไฟล์ใหม่เข้ากลุ่ม
ส่ง PDF, DOCX, CSV หรือ XLSX หลังตั้ง cutoff แล้ว รอ worker ประมวลผล จากนั้นตรวจ log:
docker compose logs --tail=200 api worker
ทดสอบไฟล์ซ้ำ
ส่ง webhook เดิมซ้ำ ต้องไม่มี FileVersion/S3 object ใหม่ จากนั้นส่งไฟล์ใหม่ที่ใช้ชื่อเดิม Explorer
ต้องคงชื่อ invoice.pdf ทั้งสองรายการ โดยแยกกันด้วย FileVersion ID และเวลา
LINE Group: ปัญหาที่พบบ่อย
- Verify ไม่ผ่าน: ตรวจ DNS, HTTPS certificate, port 443 และ route
/v1/line/webhook - HTTP 400: ตรวจ Channel secret, signature, destination, groupId และ Source type
- HTTP 503: ตรวจ IAM, Secrets Manager และฐานข้อมูล
- ไม่มี event: ตรวจ Allow bot to join groups, Use webhook และว่า OA ยังอยู่ในกลุ่ม
- ACQUISITION_FAILED: ตรวจ access token และ egress ไป
api-data.line.me - accepted: 0: event อาจเก่ากว่า cutoff หรือเป็น event ที่ไม่รองรับ
เปิด Gmail API
เข้า Google Cloud Console เลือก project ขององค์กร เปิด APIs & Services แล้ว Enable Gmail API
สร้าง Service account
- Google Cloud Console → IAM & Admin → Service Accounts → Create service account
- ใช้ service account สำหรับ Gmail ingestion โดยเฉพาะ ไม่ต้องให้ Project Owner/Editor
- เปิด Service account → Show advanced settings → Domain-wide delegation แล้วคัดลอก OAuth Client ID
- Keys → Add key → Create new key → JSON
- นำ JSON เข้า AWS Secrets Manager แล้วลบไฟล์ local เมื่อยืนยัน secret เรียบร้อย
signJwt จะต้องปรับโค้ดก่อนอนุญาตใน Google Admin
ให้ Super Admin เข้า Google Admin Console → Security → Access and data control → API controls → Manage Domain Wide Delegation → Add new จากนั้นใส่ OAuth Client ID ของ service account และอนุญาตเพียง scope:
https://www.googleapis.com/auth/gmail.readonly
ระบบจึงอ่าน attachment ได้ แต่ไม่สามารถย้าย ลบ หรือ mark read อีเมล
สร้าง label และ filter สำหรับอีเมลรับไฟล์
- เข้า Gmail ของ
→ Settings → See all settings → Labels → Create new label - ตั้งชื่อ
- Filters and Blocked Addresses → Create a new filter
- กำหนด To =
และ Has attachment - เลือก Apply the label →
แล้วสร้าง filter
สร้าง Gmail secret ใน AWS
สร้าง secret ชื่อ แบบ Other type of secret →
Plaintext แล้วครอบ Google Service Account JSON ทั้ง object ไว้ใต้
GOOGLE_SERVICE_ACCOUNT_JSON ไม่ต้องตัด field ที่ Google ให้มา:
{
"GMAIL_MAILBOX": "",
"GOOGLE_SERVICE_ACCOUNT_JSON": {
"type": "service_account",
"project_id": "<project-id>",
"private_key_id": "<key-id>",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"client_email": "<service-account-email>",
"client_id": "<oauth-client-id>",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "<google-provided-cert-url>",
"universe_domain": "googleapis.com"
}
}
ตรวจสิทธิอ่าน secret โดยไม่ print private key:
aws secretsmanager get-secret-value \
--secret-id \
--region \
--query Name \
--output text
ถ้าต้องการตรวจว่า wrapper มี key ครบโดยไม่แสดงค่าของ private key:
aws secretsmanager get-secret-value \
--secret-id \
--region \
--query SecretString \
--output text \
| jq '{GMAIL_MAILBOX, google_keys: (.GOOGLE_SERVICE_ACCOUNT_JSON | keys)}'
และคำสั่ง jq ต้องแสดงชื่อ
key โดยไม่แสดงค่า private_keyหยุด worker ก่อนตั้งจุดเริ่ม
docker compose stop worker
ขั้นตอนนี้ป้องกัน worker poll ก่อน Source มี cutoff ที่ต้องการ
สร้าง Customer หากยังไม่มี
docker compose exec -T api \
python manage.py shell -c "from ingestion.models import Customer; c, created=Customer.objects.get_or_create(slug='coreth'); print(c.id, created)"
Provision Gmail และตั้ง cutoff
docker compose exec -T api \
python manage.py provision_source \
--customer coreth \
--type GMAIL \
--external-ref \
--secret-ref aws-secretsmanager:// \
--gmail-query "to: has:attachment label:" \
--poll-interval-seconds 60 \
--gmail-start-from-now \
--name "Coreth validation Gmail"
ทดสอบ manual poll รอบว่าง โดยยังไม่เปิด worker
docker compose run --rm worker \
python manage.py poll_gmail --source <source_id>
ส่งอีเมลใหม่และตรวจผล
หลังตั้ง cutoff แล้ว ส่งอีเมลใหม่เข้า แนบ DOCX, CSV UTF-8 และ XLSX ที่ไม่มี
macro ตรวจว่า Gmail filter ใส่ label ให้ครบ แล้วสั่ง manual poll:
docker compose run --rm worker \
python manage.py poll_gmail --source <source_id>
ตรวจ canonical S3 reference จาก Django ORM โดยไม่ผูกกับ local PostgreSQL:
docker compose exec -T api \
python manage.py shell -c "from ingestion.models import FileVersion; print(list(FileVersion.objects.order_by('-created_at').values('id','original_filename','ingestion_state','trust_state','lifecycle_state','storage_bucket','storage_key','storage_version_id','sha256','size_bytes')[:10]))"จากนั้นเลือก storage_key และ storage_version_id ของรายการหนึ่งมาตรวจ exact S3
version:
aws s3api head-object \
--bucket \
--key '<storage_key>' \
--version-id '<storage_version_id>' \
--region
Poll ซ้ำเพื่อทดสอบ duplicate
รัน manual poll ด้วย source เดิมอีกครั้ง:
docker compose run --rm worker \
python manage.py poll_gmail --source <source_id>
Attachment เดิมต้องถูกนับเป็น duplicate โดยไม่ดาวน์โหลดซ้ำ และไม่สร้าง FileVersion, staging หรือ raw-ready object เพิ่ม
เปิด worker สำหรับรับอีเมลใหม่ต่อเนื่อง
เมื่อ manual validation และ duplicate test ผ่านแล้ว ให้เปิด worker เพียงตัวเดียวสำหรับ Production:
docker compose up -d worker
docker compose ps worker
docker compose logs --tail=100 worker
Gmail: ปัญหาที่พบบ่อย
- GMAIL_CREDENTIAL_INVALID: ตรวจ wrapper JSON, mailbox และ Domain-wide Delegation
- messages: 0 หลังส่งเมล: ตรวจ recipient, label, query และเวลาเครื่อง
- AccessDenied อ่าน secret: เพิ่ม secretsmanager:GetSecretValue ให้ IAM Role
- AccessDenied PutObject: ตรวจสิทธิ S3 ทั้ง staging/raw-ready/quarantine
- DOCX/XLSX ถูกปฏิเสธ: ตรวจ macro, encryption หรือโครงสร้างไฟล์เสีย
2. Infrastructure และการติดตั้ง
กรอกค่าจริงก่อน แล้วจึงทำขั้นตอนติดตั้งจาก AWS ไปจนถึง runtime ตามลำดับ
กำหนดค่าจริงที่จะใช้ก่อนเริ่ม
ทำที่: กรอกในหน้านี้ก่อนเริ่ม Step ถัดไป ค่าจะถูกบันทึกไว้ใน
localStorage ของ Browser เครื่องนี้โดยอัตโนมัติ
Copy จะคัดลอกค่าที่กรอกล่าสุด ไม่ใช่ placeholder เดิม
,
Bucket = ,
EC2 folder = , Database = , S3 Policy =
Phase A · AWS
S3 → IAM policies → Database → Secrets → EC2 Role → Instance
Phase B · EC2
SSM → AWS CLI → Docker →
Phase C · Runtime
.env → Build → PostgreSQL → ClamAV → API → Cloudflare → Worker → Validate
สร้าง S3 bucket สำหรับรับไฟล์
ทำที่: AWS Console → S3 → Create bucket
- Bucket name:
- Region:
- Object Ownership: Bucket owner enforced
- Block Public Access: เปิดทั้ง 4 ตัวเลือก
- Bucket Versioning: Enable
- Default encryption: validation ใช้ SSE-S3 (AES256) ได้ หรือใช้ SSE-KMS ตามนโยบายองค์กร
- กด Create bucket
สร้าง IAM policy สำหรับ S3
ทำที่: AWS Console → IAM → Policies → Create policy → JSON
ตั้งชื่อเช่น แล้วใช้ policy นี้:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InspectCorethBucket",
"Effect": "Allow",
"Action": [
"s3:GetBucketVersioning",
"s3:GetBucketPublicAccessBlock",
"s3:GetEncryptionConfiguration",
"s3:GetBucketOwnershipControls",
"s3:ListBucket",
"s3:ListBucketVersions",
"s3:ListBucketMultipartUploads"
],
"Resource": "arn:aws:s3:::"
},
{
"Sid": "UseCorethIngestionObjects",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:DeleteObjectVersion",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": [
"arn:aws:s3:::/staging/*",
"arn:aws:s3:::/raw-ready/*",
"arn:aws:s3:::/quarantine/*"
]
}
]
}ถ้าใช้ customer-managed KMS ให้เพิ่มสิทธิ KMS เฉพาะ key ที่ใช้ เช่น
kms:Encrypt, kms:Decrypt, kms:GenerateDataKey และ
kms:DescribeKey
สร้าง IAM policy สำหรับ Secrets Manager
ทำที่: AWS Console → IAM → Policies → Create policy → JSON
ตั้งชื่อเช่น เปลี่ยน เป็น AWS Account ID จริง:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadCorethValidationSecrets",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": [
"arn:aws:secretsmanager:::secret:-*",
"arn:aws:secretsmanager:::secret:-*",
"arn:aws:secretsmanager:::secret:-*",
"arn:aws:secretsmanager:::secret:-*"
]
}
]
}เลือก PostgreSQL และเตรียม Database ก่อนสร้าง Secrets Manager
Database mode ที่เลือก:
- Supabase Dashboard → Project → Connect / Database settings
- ถ้า EC2 ใช้ IPv4 เป็นหลัก ให้เลือก Session pooler ที่ EC2 เข้าถึงได้
- Host:
- Port:
- Database:
— สำหรับ Supabase ปกติใช้postgres - Username:
- SSL mode:
- เก็บ Database password ไว้กรอกตรงเข้า AWS Secrets Manager เท่านั้น ห้ามใส่ลงฟอร์ม localStorage
database "... " does not exist
ให้ตรวจ dbname และใช้ชื่อ database จริงของ Supabase เช่น postgres
postgres อยู่ใต้ Docker Compose profile local
- Database:
- User:
- Host ภายใน Docker network:
postgres - Port:
5432 - SSL mode:
disableเฉพาะ Docker network local - Password สร้างตอน setup local ด้วย
openssl rand -hex 32และห้าม commit
สร้าง runtime secrets ใน AWS Secrets Manager
ทำที่: AWS Console → Secrets Manager
1) Database secret: สร้าง
แบบ Other type of secret → Plaintext:
{
"username": "",
"password": "<database-password>",
"host": "",
"port": "",
"dbname": "",
"sslmode": ""
}CORETH_DATABASE_SECRET_REFPOSTGRES_LOCAL_PASSWORD จากไฟล์ .env permission 600 ได้
โดยไม่ใช้ Database secret; production ไม่ใช้แนวทางนี้
2) Django secret: สุ่มค่าบนเครื่องผู้ดูแล:
openssl rand -hex 64สร้าง secret แบบ Other type of secret → Plaintext:
{
"DJANGO_SECRET_KEY": "<random-long-secret>"
}3) Source secrets: Gmail/LINE สร้างตาม Tab 3 เมื่อเริ่มตั้ง Source โดย IAM Role ต้องอ่าน secret เหล่านั้นได้
สร้าง EC2 IAM Role และผูก policy
ทำที่: AWS Console → IAM → Roles → Create role
- Trusted entity type: AWS service
- Use case: EC2
- Role name:
- Attach
- Attach
- Attach
- สร้าง Role
สร้าง EC2 Ubuntu สำหรับรันระบบ
ทำที่: AWS Console → EC2 → Launch instance
- เลือก Ubuntu LTS
- Production ที่ใช้ Supabase แนะนำอย่างน้อย 2 vCPU / 8 GB RAM (เช่น t3a.large/t3.large) เพราะ ClamAV โหลด signature หลายล้านรายการ; 4 GB ใช้ validation ได้แต่มีโอกาสชน memory และ 2 GB ไม่แนะนำ
- Root EBS: เปิด encryption และกำหนดขนาดให้พอกับ Docker images/logs
- IAM instance profile:
- Security Group: ไม่ต้องเปิด SSH หากใช้ Systems Manager
- ถ้าใช้ Cloudflare Tunnel เป็นทางเข้าแอป ไม่ต้องเปิด inbound 8080 จาก Internet
- Outbound ต้องไป AWS, Google, LINE, Supabase (ถ้าใช้) และ Cloudflare ได้
- Launch instance และรอ Instance state = Running
เข้า EC2 ด้วย Systems Manager และตรวจ identity
ทำที่: AWS Console → EC2 → เลือก instance → Connect → Session Manager → Connect
whoami
pwd
idจาก SSM session ปกติ whoami ควรคืน user ที่ session ใช้อยู่ เช่น ssm-user
จากนั้นตรวจว่า instance เห็น IAM Role (หลังติดตั้ง AWS CLI ในขั้นถัดไปจะรันซ้ำอีกครั้ง):
ติดตั้งเครื่องมือพื้นฐานและ AWS CLI บน EC2
ทำที่: ภายใน SSM session ของ EC2
set -e
sudo apt update
sudo apt install -y curl unzip git jq ca-certificates
if ! command -v aws >/dev/null 2>&1; then
ARCH="$(uname -m)"
case "$ARCH" in
x86_64) AWS_ARCH="x86_64" ;;
aarch64|arm64) AWS_ARCH="aarch64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${AWS_ARCH}.zip" \
-o /tmp/awscliv2.zip
rm -rf /tmp/aws
unzip -q /tmp/awscliv2.zip -d /tmp
sudo /tmp/aws/install --update
rm -rf /tmp/aws /tmp/awscliv2.zip
fi
aws --version
aws sts get-caller-identityaws sts get-caller-identity ต้องเป็น assumed-role ของ ไม่ใช่ IAM userติดตั้ง Docker Engine และ Compose plugin
ทำที่: ภายใน SSM session ของ EC2
set -e
TARGET_USER="$(whoami)"
echo "Docker user: $TARGET_USER"
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
sudo systemctl enable --now docker
sudo systemctl enable containerd
sudo usermod -aG docker "$TARGET_USER"
sudo docker --version
sudo docker compose version
echo
echo "Docker installed"
echo "Added $TARGET_USER to docker group"
echo "Reconnect SSM session once before using docker without sudo"หลังคำสั่งจบ ให้ปิด SSM session แล้วเปิดใหม่หนึ่งครั้ง จากนั้นทดสอบ:
whoami
docker ps
docker compose versiondocker ได้โดยไม่ต้อง sudoสร้าง application directory และตั้ง permission
ทำที่: SSM session ใหม่ หลัง Docker group มีผล
ตั้งแต่นี้ source code และ configuration ของระบบให้อยู่ใน
set -e
TARGET_USER="$(whoami)"
APP_DIR=""
sudo install -d -m 0755 \
-o "$TARGET_USER" \
-g "$TARGET_USER" \
/home/docker
sudo install -d -m 0750 \
-o "$TARGET_USER" \
-g "$TARGET_USER" \
"$APP_DIR"
cd "$APP_DIR"
echo "Current directory: $(pwd)"
stat -c '%U:%G %a %n' /home/docker "$APP_DIR"/home/docker ควรเป็น mode 755 และ เป็น mode 750 โดย owner/group ต้องเป็น user จาก whoamichown -R /home/docker แบบกว้าง ๆ หากภายหลังมี service อื่นอยู่ใต้ /home/docker; ขั้นตอนนี้กำหนด ownership เฉพาะ directory ที่ต้องใช้.Clone source code ลง application directory
ทำที่: EC2 → SSM ภายใน
ใช้ URL ของ repository ผ่านช่องทางที่องค์กรอนุมัติ และ clone ลง directory นี้โดยตรง:
set -e
APP_DIR=""
cd "$APP_DIR"
git clone .
git status --short --branch
git rev-parse --short HEAD
pwdpwd ต้องคืน และ Git ต้องแสดง branch/commit ที่ต้องการ deployตรวจสิทธิ AWS จาก EC2 ก่อนเปิดแอป
ทำที่: EC2 → SSM
cd
aws sts get-caller-identity
aws secretsmanager get-secret-value --secret-id --region --query Name --output text
aws s3api get-bucket-versioning --bucket --region
aws s3api get-public-access-block --bucket --region
aws s3api get-bucket-encryption --bucket --region
aws s3api get-bucket-ownership-controls --bucket --region Smoke test สิทธิ S3 จาก EC2
ทำที่: EC2 → SSM
ทดสอบ Put → Head exact VersionId → Delete exact VersionId ใน staging/ ก่อนให้ worker ใช้งานจริง:
set -e
TEST_KEY="staging/iam-smoke-$(date +%s).txt"
printf 'coreth-s3-smoke-test\n' > /tmp/coreth-s3-smoke.txt
VERSION_ID=$(aws s3api put-object \
--bucket \
--key "$TEST_KEY" \
--body /tmp/coreth-s3-smoke.txt \
--region \
--query VersionId \
--output text)
echo "VersionId=$VERSION_ID"
aws s3api head-object \
--bucket \
--key "$TEST_KEY" \
--version-id "$VERSION_ID" \
--region
aws s3api delete-object \
--bucket \
--key "$TEST_KEY" \
--version-id "$VERSION_ID" \
--region
rm -f /tmp/coreth-s3-smoke.txtสร้าง .env ใน application directory
ทำที่: EC2 → SSM →
cd
cp .env.example .env
chmod 600 .env
nano .envProduction + Supabase:
CORETH_ENVIRONMENT=production
AWS_REGION=
CORETH_RAW_BUCKET=
CORETH_S3_KMS_KEY_ID=
CORETH_DATABASE_SECRET_REF=aws-secretsmanager://
CORETH_DJANGO_SECRET_KEY_REF=aws-secretsmanager://
CORETH_SITE_ADDRESS=http://localhost
CORETH_SERVICE_MODE=api
CORETH_ADMIN_PORT=8081
CORETH_SECURE_SSL_REDIRECT=false
CORETH_CSRF_TRUSTED_ORIGINS=https://
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,api,,CORETH_SECURE_SSL_REDIRECT=false
เพราะ public HTTPS จบที่ Cloudflare แต่ Tunnel เชื่อม Caddy ภายในด้วย HTTP หากเปิดค่านี้ Django จะ redirect ไป
https://localhost ส่วน Secure cookie ยังเปิดอัตโนมัติจาก CORETH_ENVIRONMENT=production.env; เก็บแค่ Secret reference และ CSRF origin ต้องตรงกับ Explorer hostnameLocal development:
LOCAL_DB_PASSWORD="$(openssl rand -hex 32)"
printf '\nPOSTGRES_LOCAL_PASSWORD=%s\n' "$LOCAL_DB_PASSWORD" >> .env
unset LOCAL_DB_PASSWORD
chmod 600 .envDatabase/User ใช้ /
ตาม Docker Compose
.env.example, Django settings
และ compose ของ repository หากมีตัวแปรเพิ่มให้ใช้ของ project เป็นหลัก
.env ต้องเป็น permission 600 และอยู่ใต้ application directory เท่านั้นจัด Docker Compose ให้ Production ไม่รัน PostgreSQL local
ทำที่: EC2 → SSM → application directory
ให้ service postgres อยู่ใน profile local เพื่อไม่ถูก start ใน Production:
services:
postgres:
image: postgres:17-alpine
profiles:
- local
environment:
POSTGRES_DB: ${POSTGRES_DB:-}
POSTGRES_USER: ${POSTGRES_USER:-}
POSTGRES_PASSWORD: ${POSTGRES_LOCAL_PASSWORD:?POSTGRES_LOCAL_PASSWORD is required}
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- backendapi, worker และ migrate
ต้องไม่ depends_on: postgres เพราะ Production ใช้ Supabase
ให้ Local เปิด postgres ก่อนด้วย --profile local แทน
cd
docker compose config --quiet
docker compose config --services
# หา dependency ที่ยังผูกกับ postgres
docker compose config | grep -n -B4 -A8 'postgres' || truedocker compose up -d
จะไม่ start service ที่อยู่ใน profile local
Build image และเปิด dependency ตาม Database mode
ทำที่: EC2 → SSM → application directory
cd
docker compose buildProduction + Supabase: ไม่เปิด local PostgreSQL
docker compose up -d redis clamav
docker compose psLocal development:
docker compose --profile local up -d postgres redis clamav
docker compose --profile local psตรวจ PostgreSQL ก่อน migrate
ทำที่: EC2 → SSM
Production + Supabase: ตรวจค่าที่เก็บใน Database secret โดยไม่แสดง password:
aws secretsmanager get-secret-value \
--secret-id \
--region \
--query SecretString \
--output text \
| jq 'del(.password)'dbname ปกติควรเป็น
cd
docker compose --profile local exec -T postgres \
pg_isready -U -d
docker compose --profile local exec -T postgres \
psql -U -d \
-c 'select version();'รัน migration
ทำที่: EC2 → SSM → application directory
cd
docker compose run --rm migrateถ้า repository ไม่มี service ชื่อ migrate ให้ใช้ command ที่ compose ของ project กำหนดจริง เช่น
python manage.py migrate --noinput ผ่าน container ของ API
migrate ต้องต่อ Supabase ผ่าน Database Secret และไม่ depend on local postgres. Local: เปิด postgres ด้วย --profile local ให้ healthy ก่อนแล้วค่อยรัน migrate.ตรวจ ClamAV, FreshClam, DNS และ egress
ทำที่: EC2 → SSM → application directory
ClamAV ต้องออก Internet เพื่ออัปเดต signature จาก database.clamav.net
หาก backend เป็น internal: true ให้ต่อ clamav เข้ากับ egress network เพิ่ม
clamav:
image: clamav/clamav:1.4
dns:
- 169.254.169.253
- 1.1.1.1
networks:
- backend
- egress
networks:
backend:
internal: true
egress:
driver: bridgeegress;
api และ worker ก็ต้องมี outbound ไป AWS / Supabase / Google / LINE ตามงานที่ใช้
cd
docker compose up -d --force-recreate clamav
docker compose logs --tail=200 clamav
docker compose exec -T clamav \
getent hosts database.clamav.net
docker compose exec -T clamav \
clamdscan --ping 3daily.cld/cvd สำเร็จ, DNS resolve ได้ และ clamdscan --ping ได้ PONG
Socket for clamd not found ต่อเนื่อง: ตรวจ free -h และ OOM log;
Production แนะนำ RAM 8 GB เพื่อให้ clamd โหลด signatures ได้เสถียร
เปิด API และ Caddy ก่อนเปิด worker
ทำที่: EC2 → SSM → application directory
cd
docker compose up -d api caddy
docker compose ps
docker compose logs --tail=100 api caddyสร้าง root user สำหรับ Explorer
ทำที่: EC2 → SSM → application directory หลัง migration สำเร็จและ API ทำงานแล้ว
เปิด service admin ซึ่ง bind เฉพาะ loopback port 8081:
docker compose --profile admin up -d admin
docker compose --profile admin psตรวจหรือสร้าง Customer ก่อน คำสั่งนี้จะไม่สร้างซ้ำหากมี slug coreth อยู่แล้ว:
cd
docker compose --profile admin exec -T admin \
python manage.py shell -c "from ingestion.models import Customer; c, created = Customer.objects.get_or_create(slug='coreth'); print({'customer_id': str(c.id), 'created': created})"สร้าง root โดยเปลี่ยนอีเมลให้เป็นอีเมลผู้ดูแลจริง:
docker compose --profile admin exec admin \
python manage.py bootstrap_root_user \
--customer coreth \
--username root \
--email root@coreth.co- คำสั่งจะถามรหัสผ่าน 2 ครั้งและไม่แสดงรหัสผ่านบนหน้าจอ
- ห้ามเติม
-Tในคำสั่งสร้าง root เพราะต้องรับรหัสผ่านจาก keyboard - ไม่มี default password ในโค้ด, Git หรือ
.env - หากลืมรหัสผ่าน ให้รันคำสั่งเดิมพร้อม
--reset-password
เมื่อ Login แล้ว root สามารถเปิดแท็บ จัดการผู้ใช้ เพื่อสร้างบัญชี ปิดบัญชี ตั้งรหัสผ่านใหม่ และกำหนดสิทธิ์ดูไฟล์ เปิดไฟล์ จัดการสถานะไฟล์ อัปโหลดไฟล์ หรือจัดการผู้ใช้ได้
ปิด Explorer โดยไม่หยุด LINE/Gmail/API:
docker compose --profile admin stop adminรัน Doctor และแยก Infrastructure FAIL ออกจาก Pre-source FAIL
ทำที่: EC2 → SSM → application directory
cd
docker compose exec -T api \
python manage.py file_ingestion_doctorก่อน provision Source ค่าที่ควรผ่านอย่างน้อย:
POSTGRESQL = PASSMIGRATIONS = PASSS3_SECURITY = PASSCLAMAV = PASSWORKER = PASSเมื่อ worker ถูกเปิด
SECRET_REFERENCES อาจยัง FAIL เพราะ configured = 0 ก่อนสร้าง LINE/Gmail Source;
DB_S3_RECONCILIATION อาจ WARN และ BACKUP/RESTORE_REHEARSAL
อาจยัง FAIL จนกว่าจะทำ operational validation ภายหลัง
สร้าง Cloudflare Access Application สำหรับ Explorer
ทำที่: Cloudflare Dashboard → Zero Trust → Access controls → Applications
cloudflared Tunnel; ไม่ต้องเปิด Mesh เพื่อให้ Explorer/Webhook ทำงาน
- Create application → Self-hosted / Self-hosted and private
- Application name:
- Public hostname:
- สร้าง Policy: Action = Allow
- Include เฉพาะอีเมล/โดเมนองค์กรที่อนุญาต เช่น
@coreth.co - เลือก Identity Provider เช่น Google Workspace หรือ One-time PIN ตามองค์กร
- ห้าม Allow Everyone สำหรับ Explorer
- Save Application ให้เสร็จก่อนผูก JWT validation ใน Tunnel route
Unable to find your Access application
สร้าง Cloudflare Tunnel และติดตั้ง cloudflared บน EC2
ทำที่: Cloudflare Dashboard → Zero Trust → Networks → Tunnels & Mesh → Create tunnel
- เลือก Tunnel แบบ Cloudflared ไม่ใช่ Mesh/WARP Connector
- ตั้งชื่อ
- หน้า Install connector: EC2 เป็น Ubuntu ให้เลือก platform Debian
- Cloudflare จะแสดง installation command พร้อม tunnel token
- Copy command ที่ Cloudflare สร้างให้จริงไป run ใน EC2 SSM session
- ห้ามบันทึก tunnel token ลง Git,
.env, HTML guide หรือ chat - รอ Tunnel / Connector status = Healthy
sudo systemctl status cloudflared --no-pager
sudo journalctl -u cloudflared -n 100 --no-pagercloudflared เป็น system service ของ EC2; application source ยังคงอยู่ที่
สร้าง Published application routes สำหรับ Explorer และ LINE webhook
ทำที่: Cloudflare Dashboard → Networks → Tunnels & Mesh →
→
Published application routes
coreth.co เช่นชื่อด้านล่าง
แทนรูปแบบ files.thinkberry-file-ingestion.coreth.co
เพื่อหลีกเลี่ยงปัญหา TLS ของ wildcard certificate ชั้นเดียว
Route 1 — Explorer
- Hostname:
- Path: เว้นว่าง
- Service type: HTTP
- Service URL:
http://localhost:8081 - HTTP Host Header:
localhost - Enforce Access JWT validation: On
- เลือก Access Application:
ที่ hostname ตรงกับ Explorer hostname นี้
Route 2 — LINE webhook
- Hostname:
- Path:
/v1/line/webhook - Service type: HTTP
- Service URL:
http://localhost:8080 - HTTP Host Header:
localhost - Enforce Access JWT validation: Off
- ไม่ผูก Access login — LINE Platform login ผ่าน Cloudflare Access ไม่ได้
- Django ต้องตรวจ
X-Line-Signatureจาก raw request body ทุก request
/v1/line/webhook
ตรวจ DNS หลัง Save:
dig @1.1.1.1 +short
dig @1.1.1.1 +short ตรวจ LINE route จากเครื่องภายนอก:
curl -i \
-X POST \
'https:///v1/line/webhook' \
-H 'Content-Type: application/json' \
-d '{}'X-Line-Signature;
ถ้าเห็น Access login = hooks ถูกผูก Access ผิด, ถ้า 502 = cloudflared ไป localhost:8080 ไม่ถึง
ตรวจ Explorer: เปิด Incognito ไปที่
https://
ต้องเจอ Cloudflare Access login ก่อน จากนั้นต้องเจอหน้า Login ของ CORETH Explorer
ให้ Login ด้วย root และตรวจว่าเห็นแท็บ ไฟล์ กับ จัดการผู้ใช้
สลับมุมมอง list/grid ได้, breadcrumb ย้อนระดับได้ และไฟล์ DOCX ดาวน์โหลดด้วยชื่อเดิม
ปิด public inbound ที่ไม่จำเป็น
ทำที่: AWS Console → EC2 → Security Groups
- ปิด inbound 8080/8443 จาก Internet
- ถ้าใช้ Cloudflare Tunnel ทั้งหมด สามารถไม่เปิด inbound 80/443 ของแอปได้
- คงช่องทางดูแลเครื่องผ่าน Systems Manager
- อย่าสร้าง DNS-only record ที่ชี้ public IP ของ EC2 สำหรับบริการนี้
ตั้ง Source จาก Tab 3 แล้วจึงเปิด worker
ทำที่: กลับไป Tab 3 ของคู่มือนี้
ทำ LINE หรือ Gmail ตาม flow ของ source นั้น โดยตั้ง cutoff/provision ให้เสร็จก่อนเปิดรับ event จริง
หลัง Source ถูกต้องแล้วกลับมาที่ EC2:
cd
docker compose up -d worker
docker compose ps
docker compose logs --tail=200 api workerFinal validation ก่อนถือว่าติดตั้งเสร็จ
ทำที่: EC2 SSM + Browser + AWS Console
cd
docker compose ps
docker compose exec -T api python manage.py file_ingestion_doctor
docker compose logs --tail=200 api worker- S3 private, encrypted, Versioning Enabled และ BucketOwnerEnforced
- EC2 ใช้ assumed IAM Role และไม่มี AWS access key ในเครื่อง
- Application อยู่ใน
และ permission ถูกต้อง .envเป็น mode 600- Database เชื่อมต่อและ migrations ผ่าน
- สร้าง root ของ Explorer แล้ว และ Login ด้วย username/password ได้
- root สร้างผู้ใช้และกำหนดสิทธิ์จัดการ user/file ได้
- ClamAV พร้อมสแกน
- Cloudflare Tunnel Healthy
- Explorer ถูก Cloudflare Access ป้องกัน
- Explorer เปิด/ปิดได้ด้วย service
adminและ bind เฉพาะ127.0.0.1:8081 - LINE/Gmail รับเฉพาะ event หลัง cutoff
- ไฟล์ผ่าน validation แล้วไป READY/CLEAN/ACTIVE
- storage bucket/key/VersionId ใน PostgreSQL ตรงกับ exact S3 version
- duplicate event ไม่สร้าง FileVersion/S3 object ใหม่
- Explorer Published route = files-thinkberry-file-ingestion.coreth.co และ Access JWT = On
- LINE Published route = hooks-thinkberry-file-ingestion.coreth.co/v1/line/webhook และ Access JWT = Off
- Production Supabase ใช้ Database Secret และไม่มี local postgres container ทำงาน