AWS 채팅앱 인프라 배포 (2/2)

🖥 Node.js Chatting App

라라벨 앱과는 달리 노드JS 앱은 Docker 컨테이너로 ECS/Fargate 으로 배포되며, Redis, DynamoDB 등의 인프라를 사용한다. 필요한 경우 읽지 않은 채팅메시지 푸시알림은 SQS 를 통해 라라벨앱으로 전달하여, 워커 환경에서 처리하도록 되어 있다.

📙 DynamoDB

chat-messages, chat-rooms, chat-users 테이블을 생성시 hash key 를 지정하고 default 옵션으로 생성한다. 생성후에는 Provisioned capacity 의 read units 과 writes units 을 10, 5 로 조정했다. (DynamoDB 는 특정 VPC 에 종속되지 않고 접근이 가능하다.)

📙 SQS

chat-queue 큐를 생성한다. (SQS 는 특정 VPC 에 종속되지 않고 접근이 가능하다.)

📙 VPC

default vpc 를 사용할 수 도 있지만, chat-vpc 라는 별도의 vpc 를 생성해야만 환경삭제시 편하다.

  • Name tag: chat-vpc
  • IPv4 CIDR block: 10.0.0.0/16
  • IPv6 CIDR block: no

📙 Subnets

ECS 클러스터 생성시 VPC 를 생성하는 옵션을 선택하는 경우, 자동으로 생성된다.
Name tag: chat-subnet-az1, chat-subnet-az2
VPC: chat-vpc
AZ: AZ1, AZ2, …
IPv4 CIDR block: 10.0.0.0/24, 10.0.1.0/24, …

📙 Redis

Redis 는 Subnet Group 을 지정해야하는데, 이것이 VPC 와 연결되어 있다. 다시 말해 채팅서버가 바라보는 Redis 를 라라벨서버가 같이 바라봐야 한다면 같은 VPC 에 생성해야하지만, 그렇지 않다면, 다른 컨디션을 고려할 필요없다. 아래의 옵션으로 생성했다.

Engine: Redis
Location: Amazon Cloud
Redis settings:

  • chat-redis
  • 6.x
  • 6379
  • default.redis6.x
  • cache.t2.small (1.55 GB)
  • # of replicas : 0
  • subnet group : create new
    • name : chat-subnet-group
    • vpc : chat-vpc
    • subnets : 위 단계에서 생성한 subnets
    • security groups :
    • chat-default-sg (자동으로 생성) : all
    • chat-redis-sg (수동으로 추가): 6379

📙 Internet Gateway

  • ECS 클러스터 생성시 VPC 를 생성하는 옵션을 선택하는 경우, 자동으로 생성됨.
  • name: chat-igw
  • VPC: chat-vpc

📙 Route Table

  • ECS 클러스터 생성시 VPC 를 생성하는 옵션을 선택하는 경우, 자동으로 생성됨.
  • VPC 에 자동으로 생성된 라우트를 수정 (Edit routes)
    • Routes
      • 탭 = 0.0.0.0/0
      • target = chat-igw
    • Subnet Associations
      • chat-subnet-az1
      • chat-subnet-az2

📙 Security Groups

  • chat-task-sg: allows 3210
  • chat-lb-sg: allows HTTP, HTTPS
  • chat-dynamodb-sg: allows DynamoDB
  • chat-redis-sg: allows Redis

📙 System Manager

Parameter Store 에 .env.production 에 지정한 환경변수들을 빠짐없이 입력한다.

📙 Load Balancer

Application Load Balancer

  • chat-load-balancer
  • internet-facing
  • ipv4
  • Listeners
    • HTTP/HTTPS (80/443 포트)
  • AZ
  • 가용 AZ 모두 선택

Security Settings

chat.lousydomain.com 에 대한 Certificate ACM 에서 설정후

Security Groups

  • chat-default
  • chat-lb-sg

Routing

  • Target group
    • name: lousy-chat-tg
    • type: IP
    • protocol: HTTP
    • port: 80
  • health check
    • HTTP
    • /health (depending on your implementation)

Register targets

  • intact

📙 ECR

  • Repository name : chat-server

📙 ECS

Create Cluster 를 선택하고 cluster template 에서 Networking only (Powered by AWS Fargate) 을 선택한다.

  • 클러스터명 : lousy-chat-cluster
  • CloudWatch Container Insights : Enable

📙 Task Definition

  • Task definition name: lousy-chat-task
  • Requires Compatibilities: Fargate
  • Task execution role : ecsTaskExecutionRole
  • Task size : 0.5G, 0.25 vCPU

Container Definitions

  • Container name: chat-server
  • image: 000000000000.dkr.ecr.ap-northeast-2.amazonaws.com/chat-server:latest
  • private repository authentication: intact (false)
  • Memory limits: intact (soft limit 128)
  • Port mappings: 3210 tcp
  • Health check: CMD-SHELL, curl -f http://localhost:3210/health || exit 1
  • Environment: intact (default values)
  • Environment variables: ASM Parameter Store 에 지정한 내용을 아래 스크린샷처럼 입력한다. 키값, valueFrom, arn (형식: arn:aws:ssm:ap-northeast-2:000000000000:parameter/chat-REGION)

📙 Service

Configure service

  • Launch type: Fargate
  • Task Definition
    • Family : lousy-chat-task
    • Revision : 1 (latest)
  • Platform version: LATEST
  • Cluster: lousy-chat-cluster
  • Service name: lousy-chat-service
  • Service type: REPLICA
  • Number of tasks: 1
  • Task Group: chat-task-group
  • Minimum healthy percent: 100
  • Maximum percent: 200

Deployments

  • Rolling update (set them as default)

Configure network

  • Cluster VPC: lousy-chat-vpc
  • Subnets: chat-subnets (ap-northeast-2a, ap-northeast-2b)
  • Security groups: chat-lb-sg (80/443), chat-task-sg (3210)
  • Auto-assign public IP: ENABLED
  • Health check grace period: 60
  • Load balancing
    • Load balancer type: Application Load Balancer
    • Load balancer name: lousy-chat-load-balancer
  • Container to load balance
    • Container name : port : lousy-container:3210
  • Production listener port: 80
    • Production listener protocol: HTTP
    • Target group name: lousy-chat-tg
    • 생성이후 https 로 리다이렉팅 룰 추가 예정
  • Auto Scaling
    • Minimum number of tasks: 1
    • Desired number of tasks: 1
    • Maximum number of tasks: 4
    • IAM role for Service Auto Scaling: ecsAutoscaleRole
  • Automatic task scaling policies
    • Scaling policy type: Target tracking
    • Policy name: scaling-cpu-policy
    • ECS service metric: ECSServiceAverageCPUUtilization
    • Target value: 75
    • rest intact

📙 Associate ACM SSL certificate with Application Load balancer

Refer) https://aws.amazon.com/premiumsupport/knowledge-center/associate-acm-certificate-alb-nlb

  • choose EC2 > Load balancer
  • add listener
  • default action
  • forward to chat-tg
  • group-level stickiness
    • enable up to 30 seconds
  • select certificate from ACM
  • update HTTP 80 traffics redirecting to HTTPS://#{host}:443/#{path}?#{query}

Route 53

  • Add a A record to lousy-chat-load-balancer

Posted by admin in Backend, 0 comments

AWS 채팅앱 인프라 배포 (1/2)

이 글은 2021년 2월2일 AWS 콘솔 페이지를 통해 배포하는 인프라 구성 노트다. MVP 용 채팅앱 인프라를 구성하면서 틈틈히 적은 내용이다. 구축하고자 하는 구성은 크게 라라벨 앱과 Node.js 앱으로 나뉜다.

🖥 Laravel App

라라벨 앱은 Elastic Beanstalk 의 Webserver 환경과 Worker 환경으로 배포되어, 전자는 앱을 위한 API 서버와 관리자 페이지 및 앱소개 페이지를 위해 사용되고, 후자는 라라벨 앱의 백그라운드 작업 및 채팅서버로부터 SQS 로 전달되는 푸시알림을 위한 큐워커로서 동작하는데 조만간 MySQL 과 Elasticsearch 와의 동기화 작업도 담당할 예정이다.

사용 인프라는, Elastic Beanstalk Worker, MySQL, Redis, SQS, DynamoDB, Elasticsearch(예정), S3, CloudFront, ACM, Route53, SNS, SMS, SES 등이다.

📙 RDS

DB instance size

  • db.t3.small

Storage

  • General Purpose (SSD)
  • 20 GiB

Connectivity

  • Default VPC
  • Public access : NO
  • VPC security group : default, RDB group

Database options

  • initial database name : wanderinghog

Monitoring

  • Audit log
  • Error log
  • General log
  • Slow query log

📙 Redis

라라벨 앱을 위한 1개 인스턴스 (cache.t2.micro 노드) 와 채팅 앱을 위한 1개 인스턴스 (cache.t2.small 노드) 를 생성한다. 주의할 점은 EB 환경과 같은 subnet group 선택후, Security groups 은 default security group 과 Redis 6379 port 를 위한 security group 을 지정한다. 다른 옵션들은 모두 leave as default.

📙 SQS

Worker 환경을 만들면서 Auto-generated queue 를 선택하면 Worker 환경을 위한 AWSEBWorkerQueue/AWSEBWorkerDeadLetterQueue 콤보가 자동으로 생성된다. 물론, 환경에 attach 된 queue 를 갖는 다는 것은 장점도 있지만 단점도 있다. 일단, 이름에 랜덤 스트링을 포함해 지저분한 건 차치하더라도, 행여나 환경 리빌딩이라도 하는 경우엔 .env 에 지정한 queue 이름과 달라져 오류를 만들어낼 수 밖에 없다. 따라서 수동으로 큐를 생성하는 것이 더 낫다. 이후 채팅 앱을 위한 queue 와 Elasticsearch 동기화를 위한 queue 각각 1개씩을 생성할 것이다. 생성후 Access policy (Permissions) 내용을 삭제하면, 아래와 같은 기본 퍼미션이 자동으로 만들어진다.

{
  "Version": "2012-10-17",
  "Id": "arn:aws:sqs:ap-northeast-2:206020602060:lousydomain-queue/SQSDefaultPolicy"
}

📙 S3

Private bucket for configuration files

production 용 .env 파일을 비롯하여, Google API 사용을 위하여 필요한 credentials 을 비공개로 저장하고 있다가 .ebextentions 배포 스크립트가 코드베이스의 특정위치에 복사하기 위한 파일들을 저장하는 버킷이 필요하다. 버킷 이름은 environment 라 명명했고 permissions 은 당연히 Block all public access 옵션을 선택했다.

Public bucket for Laravel app

라라벨앱에서 저장할 이미지 버킷의 objects 들은 누구나 볼 수 있어야 하기 때문에 퍼미션을 아래와 같은 설정했다.

Block public access options

Bucket policy 는 아래와 같고,

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::206020602060:user/admin"
            },
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::pub-images",
                "arn:aws:s3:::pub-images/*"
            ]
        }
    ]
}

아래 그림과 같이, ACL 에서 Everyone 에게 Read 퍼미션을 등록했다.

S3 Bucket ACL

📙 Elastic Beanstalk (Web server)

make sure all the scripts in .ebextensions and .platform are written correctly for the latest Elastic Beanstalk requirements.

우선, Select environment tier 항목에서 Web server environment 를 선택 후 Configure more options 버튼을 클릭한다. Presets 는 Auto scaling 을 위한 High availability 옵션을 선택한다.

Software

Document root 는 /public

Instance log streaming to CloudWatch Logs

  • enabled
  • 5 days retention
  • delete logs upon termination

Environment properties 에 아래 entries 추가

  • COMPOSER_HOME : /root
  • WORKER : false
  • TZ : Asia/Seoul

Instances

EC2 security groups

  • default security group
  • eb-80/22 : VPC security group

Capacity

Leave as default

Load balancer

Leave as default

Rolling updates and deployments

  • Deployment policy : Rolling
  • Batch size : 30%
  • Rolling based on Health
  • Batch size : 1
  • Minimum capacity : 1

Security

  • Virtual machine permissions : EC2 key pair : aws-eb

Monitoring

Health monitoring rule customization

  • Ignore application 4xx : enabled

Managed Updates

Weekly update window

  • Tuesday 19:00 UTC (04:00am)

Notifications

Network

Load balancer settings : set Visibility to Internal

Load balancer subnets (everything but ap-northeast-2b/2d, which has no support for t2.micro instance type)

  • ap-northeast-2a
  • ap-northeast-2c

Instance subnets

  • ap-northeast-2a
  • ap-northeast-2c

Database

leave as default

Tags

leave as default

📙 Elastic Beanstalk (Worker)

Note that I’d used Elastic Beanstalk Worker environment with this package (https://github.com/dusterio/laravel-aws-worker) for my earlier projects because of the less complexity. However, convenience comes with a price. With the approach, having multiple queue connections and dedicated workers must go through extra overheads. So, instead, I decided to go with supervisord this time. Here’s how.

이젠 Worker 환경을 셋업하는데, Select environment tier 항목에서 Worker environment 를 선택 후 Configure more options 버튼을 클릭한다. Presets 는 Auto scaling 을 위한 High availability 옵션을 선택한다.

Software

Document root 는 /public

Instance log streaming to CloudWatch Logs

  • enabled
  • 5 days retention
  • delete logs upon termination

Environment properties 에 아래 entries 추가

  • COMPOSER_HOME : /root
  • WORKER : true
  • TZ : Asia/Seoul

Instances

EC2 security groups (원칙적으로 Worker 의 경우 22 만 열면 된다. worker-daemon 에서 localhost:80 으로 요청을 보낸다. https://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/using-features-managing-env-tiers.html 또는 https://github.com/dusterio/laravel-aws-worker 참고)

  • default security group
  • eb-22 : VPC security group

Capacity

Leave as default

Worker

  • Worker 환경은 원칙적으로 웹서버 프로세스가 필요없지만, Elastic Beanstalk 의 경우, aws-sqsd 데몬 프로세스와 puma 를 사용하여 SQS 에서 읽은 아이템 내용을 localhost:80/path 로 POST 호출하여 전달하는 기능을 기본으로 제공한다. aws-sqsd 프로세스를 없애면, health 이상으로 보고 되기 때문에, aws-sqsd 가 사용하지 않는 빈 SQS 큐를 바라보도록 우회시켰다. 대신 다중 큐 커넥션처리를 위하여 supervisord 를 설치하여 사용한다. (이런 낭비를 막기위해선 결국 라라벨앱 역시 Docker image 를 만들어서 ECS/Fargate 로 운영하는 방법이 답인듯).
  • Worker queue : aws-sqsd 데몬 프로세스가 바라볼 큐를 지정하는 옵션이므로, lousy-void-queue 를 만들어 queue 를 지정한다. (SQS 의 경우, 이처럼 만들어만 놓고 사용하지 않는 경우 사용료가 발생하지 않는다.) aws-sqsd 우회가 목적이므로, HTTP path 는 /void 로, 최대 동시 접속 HTTP connections 을 1로 설정했다.

Rolling updates and deployments

Leave as default

Security

  • Virtual machine permissions : EC2 key pair : aws-eb

Monitoring

Health monitoring rule customization

  • Ignore application 4xx : enabled

Managed Updates

Weekly update window

  • Tuesday 19:00 UTC (04:00am)

Notifications

Network

Load balancer subnets (everything but ap-northeast-2b/2d, which has no support for t2.micro instance type)

  • ap-northeast-2a
  • ap-northeast-2c

Instance subnets

  • ap-northeast-2a
  • ap-northeast-2c

Database

leave as default

Tags

leave as default

Tip) 만일 라라벨 db:seed 명령중에 메모리 부족 오류가 난다면 커맨드라인 상에서 아래와 같이 swap 메모리를 2G 정도 잡아두고 재실행해볼 수 있겠다. (실제론 Worker 환경의 인스턴스를 t2-tiny (1G) 에서 t2-small (2G) 로 변경했다.)

sudo dd if=/dev/zero of=/swapfile bs=128M count=16
sudo chown root:root /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo swapon -a

📙 Route53

MySQL, Redis, SQS 가 배포된 상태에서 동작되는 라라벨 앱을 확인할 수 있으면 도메인명을 연결할 차례이다. Route53 > Hosted zones 에 도메인명을 생성 후 NS 레코드에 나오는 4개의 name servers 정보를 godaddy 와 같은 domain name registrar 콘솔 페이지에 name servers 로 등록한다.

A record

EB 환경 셋업시 생성한 Load Balancer 를 지정하는 방법은 새로운 콘솔화면에서, Alias 스위치를 켜고 Alias to Elastic Beanstalk environment 를 선택하고 지역과 LB 이름을 셀렉션박스에서 고르도록 되어있다.

CNAME record

www.lousydomain.com to lousydomain.com

📙 Certificate Manager

ACM 의 역할은 자동으로 https certificate 를 DNS 에 연결할 Route53 레코드 자동생성 및 주기별 자동 갱신이다. Request a certificate 버튼을 누르고 이후 페이지에서 request a public certificate 을 선택 후 도메인명을 지정하면, Route53 에 CNAME 레코드를 자동으로 생성시킬 수 있다.

List 에 Imported type 의 server 라는 도메인이 나오는데, 이는 뭔가 모르겠다. In use? 가 Yes 던데…

📙 EC2

Load balancers

기본적으로 생성된 로드밸런서의 경우, HTTP 를 위한 80 포워딩 리스너 밖에 등록이 되어 있지 않으므로, HTTPS 를 위한 443 리스너 등록이 필요하다. 일단, 80 리스너는 HTTPS 로의 리다이렉팅, 443 리스너는 EB 타겟그룹으로의 포워딩을 지정하고, Security policy 에는 ELBSecurityPolicy-2016-08 옵션을 Default SSL certificate 에는 From ACM 과 도메인명을 선택한다.

아래의 이미지와 비슷한 셋팅이 이뤄졌으면 정상이다.

EC2 > Load balancer 설정화면

참고로 2번째 HTTPS 리스너의 경우 오랜지색 exclamation mark 가 있는데, 이는 security group 에 443 포트가 허용되어 있지 않다는 경고이므로, LB 의 SG 을 선택하여 수정한다.

📙 CloudFront

이제 CDN 설정한다. Create Distribution 버튼을 클릭하고 Get started 버튼을 클릭하면, 다양한 옵션들을 입력하게끔 되어있는데, 스크린샷으로 설명을 대처하겠다. 참고로 use-case 는 App 에 제공할 이미지 CDN 설정이다.

Origin Settings

Default Cache Behavior Settings

Distribution Settings

Tip) 깨알글씨를 읽어보면 알 수 있듯 cdn.lousydomain.com 와 같은 주소로 CloudFront 를 사용하고 싶다면 해당 도메인명과 Custom SSL certificate request 를 미동부 리젼(us-east-1) 의 ACM 에 해야만 하는 조건이 있다. 따라서, 다른 디테일 옵션들을 지정하기 전, 반드시 먼저 SSL certificate 신청을 완료하고 CloudFront 신청 페이지를 refresh 해야만 cdn.lousydomain.com 이라는 옵션이 Custom SSL Certificate 아래 입력박스안에 나타난다.

CloudFront Distributions 리스트에서 아이디를 클릭하면 General 탭에 정보가 나타나는데, d312cokiv00ltd.cloudfront.net 와 같은 값을 갖는 Domain Name 을 찾아 Route 53에서, cdn.lousydomain.com 에 대한 CNAME 레코드 값으로 반드시 등록해야 한다.

📙 SES

Simple Email Service 는 이메일 발송만 할 것이고, 이메일수신은 ForwardEmail 서비스를 이용하여, 다른 계정으로 포워딩되도록 셋팅했다. 이를 위해선 MX 와 TXT 레코드를 Route53 에 설정해야하는데, 그 과정 설명은 해당 서비스 페이지에 상세한 설명이 나오므로 생략한다.

Identity Management

이메일 발송을 위해서는 도메인명과 이메일을 확인해야한다. Verify a New Domain 버튼을 클릭 후 도메인명을 입력한다. Generate DKIM Settings 를 선택하면, 자동으로 Route 53 에 레코드들을 자동으로 생성할 수 있다. 이렇게 도메인명을 확인했으면 발송 이메일의 주소를 입력하여 소유권을 확인한다.

Tip) 이메일 Notifications 셋업은 email forwarding 을 enable 시키도록 한다. 그러면 complaints, bounce 등에 대한 보고를 이메일로 받을 수 있다.

Email Sending

Sending Statistics 메뉴를 클릭후 Sandbox 모드를 프로덕션모드로 변경하는 요청을 하면 수시간내에 프로덕션 모드로 기본 1일 5만건의 발송을 quota 를 할당받을 수 있는데, 여기까지하면 이메일을 발송할 수 있다.

📙 SNS

SNS 의 경우, EB 환경 생성시 Notification 이메일 주소를 지정하면 자동 생성되며, endpoint 는 email 로 설정되어 있으므로 Request confirmation 버튼을 눌러서 수신 확인을 한다. 이 email 로 EB 환경 상태 변경시 마다 알림이 전달된다.

SNS 의 Push Notification 도 필요하므로 Firebase FCM 을 설정한 뒤, arn:aws:sns:ap-northeast-2:000000000666:app/GCM/lousy-domain 과 같은 arn 생성하여 라라벨앱에서 사용하도록 한다.

Posted by admin in Backend, 0 comments

Realtime broadcasting with Laravel and NuxtJS (4/4)

한동안 여러가지 프로젝트 작업들을 동시에 진행하는 상황이였기 때문에, 온갖 개인적인 SNS 활동과 블로깅을 완전히 멈춘지 꽤 오랜 시간이 지난 것 같다. 이번 주제글에 대한 결론을 작성하지 못했기 때문에 그동안에 작업 경험들을 근간으로 생각들을 정리해 본다. 세부 디테일을 정리하기 보단 insight 를 공유하고자 한다.

일단, 초기에 실시간 동적 변경을 위한 브로드캐스팅 기능에 포커스가 채팅기능으로 옮겨갔기 때문에, Laravel Echo 를 채팅구현에 이용할 수 있을까하는 고민을 해봤으나, 해당 패키지가 타게팅을 하는 유즈케이스가 채팅에 포커스를 두고 있다는 단점이다. Echo 는 일반적인 알림 기능이나 UI 변경시 사용하기 적합한 패키지이다. 또한 Flutter 를 사용하는 경우 아직 완벽한 package 가 없다는 단점도 존재한다.

호미로 밭을 갈 수 도 있겠으나, 보다 use-case 에 적합한 dedicated solution 을 사용하는 것이 상식적인 판단이란 말이다. 그리하여 커스텀 solution 을 직접 구현하기로 하였는데, AWS 을 사용하는 관계로, NodeJS 와 아래의 인프라를 사용하여 auto-scaling 이 가능한 채팅 서비스를 구축했고, 읽은 않은 메시지 push message 발송은 SQS 로 payload 를 전달하여, Laravel app 에서 SNS 을 이용하여 발송시키도록 했다.

  • DynamoDB
  • SQS
  • ECS/Fargate
  • ALB
  • ElastiCache
  • ACM

다음번엔 실제 운영중인 데이팅앱의 AWS 인프라 배포를 콘솔로 진행한 내용을 정리해보겠다.

Posted by admin in Backend, 0 comments

Realtime broadcasting with Laravel and NuxtJS (3/4)

A chat application using Laravel Echo server?

If you follow along, you’d know that making a chat application using Echo server is possible but, not quite ideal ’cause it’s not what it’s built for. It’s more of a general purpose socket.io server with some Laravel specific goodies like user authentication for private channel and notification channel scaffolding.

When you look into the Echo server’s “out of the box” event publishing feature, there’re 2 different ways of doing it.

First off, Redis PUBSUB using psubscribe() API

If you want to send an event this way, you need to broadcast(new LaravelEvent) on a Laravel server to publish it to the Redis channel specified in the LaravelEvent. You see it constantly involves 4 hop relays. 1) A Nuxt app calls Laravel API, 2) Laravel API dispatches the broadcast event via a queue, 3) Laravel worker retrieves it, publish it to Redis using Laravel’s default Redis configuration, 4) Redis client on Echo server receives it and Socket.io client on the same server handles the payload accordingly. You get the picture.

Secondly, HTTP call to Echo server

Like the following JS snippet, all it takes is an HTTP call and Socket.io client on Echo server handles it. That’s it. It is way simpler than the previous one as we don’t need the communication layers in the middle. Nice…

  async chat({ commit }, params) {
    const appId = 'bbbbccccddddeeee'
    const authKey = '33334444555566667777888899990000'
    const socketId = this.$echo.socketId()
    const roomId = params.room
    const adapter = this.$axios.create({
      baseURL: `http://localhost:6001`,
      headers: {
        common: {
          Authorization: authKey,
          Accept: 'application/json;charset=UTF-8',
          'Content-Type': 'application/json',
          'X-Socket-ID': socketId
        }
      }
    })

    await adapter.$post(`/apps/${appId}/events`, {
      channel: `private-chat.${roomId}`,
      name: 'App\\Events\\ChatMessageSent',
      data: {
        room: roomId,
        payload: params.payload
      },
      socket_id: socketId
    })
  },

I’m Sold.

Things that you have to go through to make use of MongoDB on AWS isn’t as smooth as you might think. So I’d rather settle down with the following snippet inside of docker-compose.yml within my local Laradock folder to play with DynamoDB this time. Even though I prefer MongoDB over DynamoDB, taking care of any kinda infra-structure myself would be a pain in the ass. The admin panel (https://github.com/aaronshaf/dynamodb-admin) that I’ve already installed on my 🍎Mac seems fine too.

### AWS DynamoDB Local #####################################
    dynamodb-local:
      image: amazon/dynamodb-local
      command: -jar DynamoDBLocal.jar -dbPath /var/opt/dynamodb/data -sharedDb
      volumes:
        - ${DATA_PATH_HOST}/dynamodb/data:/var/opt/dynamodb/data
      ports:
        - "8000:8000"
      networks:
        - backend

Now what?

Hold on, I could completely ditch the Echo or, partially tweak it to get the most out of it. I’ll have to take a close look at how things work in Echo server first. A couple of things that I want to address with it;

  • An efficient chat server preferably built on top of Laravel Echo server
  • All the bells and whistles of modern chat app features
    • user online status
    • previous message history (save/load data)
    • read flag of each message (read cursors?)
    • unread message count
    • user notification hook (if he/she misses it)
    • horizontal scalability

Posted by admin in Backend, 0 comments

Realtime broadcasting with Laravel and NuxtJS (2/4)

Local Setup

일단, Laravel 6.16 와 NuxtJS 2.11 를 사용한 “out of the box” Laravel 브로드캐스트 기능이 로컬 환경에서 작동하도록 설정 후, 전반적인 구현 디테일을 분석해 본다.

사용자가 브라우저로 접속할 frontend 주소는 localhost:3000 이고, Laradock 으로 설정한 backend API 는 amuse.test 라는 주소 값을 갖고 있으며, Echo 서버는 websockets.test:6001 로 접근가능한 구성이다. 아래의 그림을 참고하기 바란다. Laradock 환경에서, Echo 서버가 amuse.test 으로, 접근할 수 있도록 설정하는 것이 tricky 할 수 있는데 이는 이전 Laradock 관련 포스팅을 참고하기 바란다. (https://whatsupkorea.com/2018/05/26/laradock-with-muliple-projects)

Laravel 브로드캐스트 기능을 NuxtJS 를 사용하는 프로젝트에서 사용하기 위해서는, Laravel Echo 의 NuxtJS 용 wrapper 모듈 @nuxtjs/laravel-echo 을 설치 후, nuxt.config.js 파일을 아래와 같이 설정한다.

  :
  buildModules: [
    '@nuxtjs/dotenv',
    '@nuxtjs/eslint-module',
    '@nuxtjs/vuetify',
    [
      '@nuxtjs/laravel-echo',
      {
        broadcaster: 'socket.io',
        host: 'http://websockets.test:6001',
        plugins: ['~/plugins/echo.js'],
        // transports: ['websocket', 'polling'],
        authModule: true,
        connectOnLogin: true,
        disconnectOnLogout: true
      }
    ],
  ],
  :

plugins/echo.js 에는 아래와 같이 public 채널, Laravel notification 용 프라이빗 채널, 그리고 presence 채널을 위한 3개의 리스너를 추가했다. 각각의 채널은 use-case 별로 추가확장 할 수 있으므로, 가장 심플한 형태의 working example 로 보면 되겠다.

export default function({ $auth, $echo, store, app: { $toast } }) {
  $echo.channel('public').listen('PublicMessageSent', (event) => {
    console.log('data from public', event)
  })

  if ($auth.loggedIn) {
    $echo
      .private(`App.Models.User.${$auth.user.id}`)
      .notification((notification) => {
        console.log(notification)
        $toast.success(notification.message)
        store.commit('notifications/INCREASE_UNREAD_COUNT')
      })

    $echo
      .join('presence')
      .here((users) => {
        store.commit('online/SET_USERS', users)
      })
      .joining((user) => {
        store.commit('online/JOIN_USER', user)
      })
      .leaving((user) => {
        store.commit('online/LEAVE_USER', user)
      })
  }
}

2. Laravel Echo Server 를 위한 laravel-echo-server.json 파일의 내용은 아래와 같이 설정한다.

참고로, Laravel 의 config/database.php Redis 옵션의 prefix 값 즉, config('database.redis.options.prefix') 의 값이 app 내 사용한 채널명과 조합하여 사용되므로, 이를 frontend 에서도 동일한 이름으로 해당 채널을 지정하려면 databaseConfig.redis.keyPrefix 옵션을 같은 값으로 셋팅해주는 것이 좋다. 그러면, Redis 에 실제 설정된 키값에 상관없이 PHP 코드에서는 new PrivateChannel('chat'); 이라 사용하고, JS 코드에서는 Echo.private('chat').listen() 라고 사용하는 것이 가능하다.

{
  "authHost": "http://amuse.test",
  "authEndpoint": "/broadcasting/auth",
  "clients": [
    {
      "appId": "1111aaaa9999cccc",
      "key": "8888dddd2222bbbb4444ffff6666eeee"
    }
  ],
  "database": "redis",
  "databaseConfig": {
    "redis": {
      "port": "6379",
      "host": "redis",
      "keyPrefix": "laravel_database_"
    },
    "sqlite": {
      "databasePath": "/database/laravel-echo-server.sqlite"
    }
  },
  "devMode": true,
  "host": null,
  "port": "6001",
  "protocol": "http",
  "socketio": {},
  "secureOptions": 66666666,
  "sslCertPath": "",
  "sslKeyPath": "",
  "sslCertChainPath": "",
  "sslPassphrase": "",
  "subscribers": {
    "http": true,
    "redis": true
  },
  "apiOriginAllow": {
    "allowCors": true,
    "allowOrigin": "http://localhost:3000",
    "allowMethods": "GET, POST",
    "allowHeaders": "Origin, Content-Type, X-Auth-Token, X-Requested-With, Accept, Authorization, X-CSRF-TOKEN, X-Socket-Id"
  }
}

3. Laravel 7 이 아니라면, CORS 설정을 위해 "fruitcake/laravel-cors": "^1.0" 디펜던시를 수동으로 추가하고, config/cors.php를 아래와 같이 설정한다.

<?php

return [
    'paths' => ['*'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['*'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => false,
    'max_age' => false,
    'supports_credentials' => false,
];

위의 설정으로 public, private, presence 채널 접속이 가능하며, Load balancer 를 Echo 서버 앞에 설정하는 구성이라면 HTTPS 를 통신을 위한 Echo 서버의 추가 설정 또한 필요없어진다. 이제 NuxtJS 앱에서, 동적으로 프라이빗 chat.{room id#} 채널을 listen() 하거나 leave() 하면서 웹소켓통신을 할 수 있도록 만들면, 비교적 수월하게 아래 그림과 같은 채팅서비스를 구현할 수 있다.

그럼 당장 Laravel Echo 기능을 이용한 채팅 서비스를 프로덕션에 적용해보자. 라고 할 줄 알았다면… Not so fast.

Posted by admin in Backend, 0 comments

Realtime broadcasting with Laravel and NuxtJS (1/4)

Prologue

Laravel 은 웹소켓을 사용한 실시간 브로드캐스트 기능을 “out of the box” 로 지원한다. 이를 위해 Laravel Echo (https://github.com/laravel/echo) 라 하는 클라이언트 라이브러리가 프론트앤드 패키지의 일부로 제공되는데, 이를 Nuxt 프로젝트에서 사용할 수 있도록 만든 wrapper 모듈인 @nuxtjs/laravel-echo (https://github.com/nuxt-community/laravel-echo) 를 이용하면 Nuxt 프로젝트에서도 쉽게 리얼타임 기능을 사용할 수 있다.

공식 문서에 언급되어 있듯, Laravel 에서 기본으로 제공하는 브로드캐스트 드라이버는 Pusher Channel 과 Redis 가 있는데, Pusher Channel 의 경우 트래픽이 많은 프로덕션 환경에서 사용하기는 비용적인 문제가 있어 가성비를 경쟁력으로 삼아야 하는 스타트업 인 경우, 썩 좋은 옵션으로 보이지 않았다. 더우기 채팅기능을 위해 Pusher Chatkit 까지 사용해야 한다면, 가격 부담이 더욱 커지게 느껴질 것이다.

Pusher (https://pusher.com) 의 다른 대안으로는 Pusher API 를 PHP Ratchet (http://socketo.me) 을 이용하여 개발한 Laravel WebSockets (https://docs.beyondco.de/laravel-websockets) 을 사용하는 방법이 있다. 이 패키지를 사용하여 웹소켓 서버 박스를 만든 다음, Laravel 의 Pusher 브로드캐스트 드라이버가, Pusher 서버가 아닌, 커스텀 짝퉁 pusher 박스를 이용하도록 만드는 방법이다. (step by step 설명은 아래 유튜브 강의 참고.)

하지만, 이 또한 트래픽이 많아져서 해당 서비스의 horizontal 스케일이 필요할 때까지 고려한다면, 프로덕션용으로 viable path 가 될 지 걱정스런 부분이 있다. horizontal 스케일을 위해, ELB 아래에 여러 서비스 인스턴스를 연결하게 된다면, 결국 peer 인스턴스들 간에 데이터 동기화를 위한 로직이 필요할텐데, 이러한 커스터마이징을 위하여, PHP Ratchet 을 사용하는 방법이 Node.js Express 를 사용하는 방법 보다 더 나은 path 인지 여러 이유에서 확신이 서지 않았다.

또 다른 대안으로는 firebase 를 사용하는 방법인데, 옆 사무실에서는 잘 사용하고 있다지만, 이 역시 트래픽 증가를 고민할 단계 쯤 되면 viable 한 솔루션이 되지 않는다는 부정적인 견해가 팽배하여, 기술적인 부분을 깊게 알아보지 않고 쉽게 포기해 버렸다. 물론, 어떠한 문제들도 영리한 work around 방법들이 존재하겠지만, 현재 내가 갖고 있는 리소스들을 고려해봤을때 Laravel Echo Server (https://github.com/tlaverdure/laravel-echo-server) 라는 오픈소스 프로젝트를 근간으로 개발하는 것이 가장 현명한 방법이 되리라 판단했다.

서두가 길었는데, 결국 위에서 언급한 이유들로 인하여, AWS 환경에서 ALB 와 AutoScaling 기능을 이용하여 horizontal 스케일이 가능한 Redis 와 NoSQL (MongoDB 나 DynamoDB 중 하나) 을 사용하는 Laravel Echo 호환 node.js 서버의 개발하려고 한다. 다만, 이를 위한 아래에 열거된 pre-requisite 들이 Laravel/Nuxt 개발자인 나에게 만만치 않아 보이지만 말이다.

  • node.js Express 학습
  • TypeScript 학습
  • Laravel Echo Server 프로젝트 소스 분석
  • Chat 용 인증 메카니즘
  • Redis pub/sub 용 데이터 분석/설계
  • RDB/MongoDB 용 스키마 설계
  • horizontal 스케일 가능하도록 부분 변경
  • 테스트/배포

다행인 것은 비슷한 주제의 다양한 레퍼런스 자료들은 즐비하다는 것.

Learning new things

Node.js 학습은 일단 Udemy 강좌 중 Node.js, Express, MongoDB & more: The Complete Bootcamp 를 선택해서 들었는데, 내용이 42시간이 넘는 정도의 분량이라, 짧은 시간내에 모두 듣기엔 좀 인내심이 필요하다. 하루 8시간 씩 듣는다 쳐도 6일 분량. 모노토너스한 목소리 톤 때문에 깜박 졸았다면, 어흑… 섹션 9까지 듣고, 아래 섹션들은 일단 패스한다. 시간있을 때 다시!

https://www.udemy.com/course/nodejs-express-mongodb-bootcamp

  • 섹션 10. 인증
  • 섹션 11. 몽구스 모델링
  • 섹션 12. pug 템플릿 엔진
  • 섹션 13. 고급 기능
  • 섹션 14. 디플로이먼트

TypeScript 는 Frontend Masters 의 강좌인 TypeScript 3 Fundamentals, v2 를 선택해서 들었다.

https://frontendmasters.com/courses/typescript-v2/

이 강의는 4시간 분량으로 오전 작업 후 남은 하루 일정에 도전해 볼 만한 분량이다. TS 의 주된 특징들을 설명하면서 넘어가는 것까지는 좋았는데, 강의 내용을 기초로 실제 프로젝트를 시작하기 부족함이 느껴져서 다른 강의를 하나 더 듣게 되었다. 아래의 Pluralsight 기초 TypeScript 강좌. 이러다가 조낸 강의만 듣다가 에너지를 소진하고 끝내버릴 분위기…

https://www.pluralsight.com/courses/typescript-projects-configuring-compiling-debugging

Time to get my hands dirty

늘어진 스케쥴로 인해 제대로 된 path 를 가고 있는지 확신이 줄어들긴 했지만, 일단 VueJS 를 위한 JS 코딩 스타일에 맞춰진 나의 Visual Studio Code 의 ESLint 와 Prettier 옵션부터 TypeScript 를 위해 수정했다. .ts 파일 저장도 제대로 안되면 안되므로. 어쨌든, Visual Studio Code 의 settings.json 은 아래와 같이 변경하여, TypeScript 를 대응하도록 했다.

{
  "": {
    "editor.formatOnSave": false
  },
  "": {
    "editor.formatOnSave": false
  },
  "[javascriptreact]": {
    "editor.formatOnSave": false
  },
  "[typescript]": {
    "editor.formatOnSave": false
  },
  "[typescriptreact]": {
    "editor.formatOnSave": false
  },
  "": {
    "editor.formatOnSave": false
  },
  "diffEditor.renderSideBySide": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "editor.formatOnSave": true,
  "editor.formatOnPaste": true,
  "editor.largeFileOptimizations": false,
  "editor.minimap.enabled": false,
  "editor.tabSize": 2,
  "eslint.alwaysShowStatus": true,
  "eslint.validate": [
    "vue",
    "html",
    "javascript",
    "typescript",
    "typescriptreact"
  ],
  "explorer.confirmDelete": false,
  "explorer.openEditors.visible": 0,
  "files.associations": {
    "*.module": "php"
  },
  "files.maxMemoryForLargeFilesMB": 20480,
  "php.suggest.basic": false,
  "phpcs.enable": false,
  "prettier.vueIndentScriptAndStyle": true,
  "sync.gist": "e38a9bd8b19391bb6ce89814e8e621f1",
  "terminal.integrated.fontFamily": "D2Coding",
  "terminal.integrated.fontSize": 13,
  "terminal.integrated.shell.osx": "/bin/zsh",
  "vetur.completion.useScaffoldSnippets": false,
  "vetur.validation.template": false,
  "window.zoomLevel": 0,
  "workbench.colorTheme": "Monokai",
  "workbench.iconTheme": "vscode-icons",
  "editor.detectIndentation": false,
  "html.format.preserveNewLines": false,
  "eslint.probe": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue"
  ]
}

Laradock to the rescue

다음 편에서는 로컬환경에서 Laradock 을 사용하여, 프로덕션과 비슷한 환경으로Frontend, Backend 및 Laravel Echo Server 를 설정해 보겠다.

Posted by admin in Backend, 0 comments