Auto scaling Laravel App with CodeDeploy

Auto scaling Laravel App with CodeDeploy

Auto-scaling Laravel App with GitLab CI and AWS Code Deploy

·

7 min read

ဒီနေ့ ပြောပြပေးမှာကတော့ Laravel app တစ်ခုကို AWS Application Load Balancer အောက်မှာ EC2 Autoscaling သုံးပြီး AWS CodeDeploy ကနေ Blue/Green deplotyment လုပ်တဲ့နည်းပဲ ဖြစ်ပါတယ်။

ဒီ topic က အရင်ရက်က ပြောခဲ့တာတွေထက်စာရင် နည်းနည်း advance ပိုဖြစ်တဲ့ အတွက်၊ ဒီ ဆောင်းပါးကို ဖတ်မယ်ဆိုရင် AWS services တွေနဲ့ ရင်းနှီးပြီးသား လူဖြစ်ဖို့လို့ပါတယ်။ မရင်းနှီးသေးရင်တော့ ဒီဂိမ်း (About AWS Cloud Quest) ကို အရင်ဆော့ပြီး ရင်နှီးသွားအောင်လုပ်ဖို့ အကြံပေးပါရစေ။

Laravel backend ကို Auto Scaling သုံးမယ်ဆိုရင် ဒီအချက်တွေ သတိထားရပါတယ်။

  1. Cache , Session driver တွေကို redis သုံးသင့်ပါတယ်၊ redis ကိုလည်း သီးသန့် server မှာ ထားရပါမယ်၊ ဖြစ်နိုင်ရင် AWS ElasticCache သုံးသင့်ပါတယ်။

  2. Database ကိုလည်း သီးသန့် server မှာထားသင့်ပါတယ်၊ ဖြစ်နိုင်ရင် RDS သုံးသင့်ပါတယ်။

  3. Storage Driver ကိုလည်း S3 သုံးသင့်ပါတယ်။

  4. Schedule တွေ duplicate မဖြစ်ဖို့ onOneServer() function သင့်ပါတယ်။

ဒီအချက်တွေအတိုင်း လုပ်ထားမှ Server သုံးလေးလုံးမျှပြီး အလုပ်လုပ်တဲ့ အခါ အဆင်ပြေပြေ လုပ်နိုင်မှာ ဖြစ်ပါတယ်။ ဒီနေ့က Deployment ပိုင်းကို အဓိက ပြောမှာ ဖြစ်တဲ့အတွက် အပေါ်က Services တွေ Laravel ထဲမှာ configure လုပ်တဲ့အပိုင်းကို တော့ ထည့်မပြောတော့ပါဘူး။

Required AWS Services

ဒီ Deployment အတွက် အသုံးပြုသွားမဲ့ AWS Services တွေကတော့

  • S3 bucket

    • Deployment file တွေနဲ့ config တွေ သိမ်းဖို့
  • IAM user (GitlabDeployer)

    • deployment package ကို S3 ပေါ် တင်ဖို့နဲ့ CodeDeploy app ကို start လုပ်ဖို့
  • IAM service role for EC2 Instance

    • Deployment S3 bucket ပေါ်က ဖိုင်တွေကို Read access ရဖို့
  • IAM service role for CodeDeploy

    • CodeDeploy application ကို စီမံခွင့်ရဖို့
  • EC2 ALB

  • EC2 Target Group

  • EC2 Launch Template

  • EC2 Auto Scaling Group

  • CodeDeploy Application and Group

စတဲ့ services တွေကို လိုအပ်ပါတယ်။

Setup S3

  1. S3 မှာ bucket တစ်ခု ဆောက်ပါ၊ region ကို singapore ရွေးပြီး ကျန်တာတွေအကုန်လုံး ဒီအတိုင်းပဲထားပါ။ ကျွန်တော်တို့ ဒီဆောင်းပါးမှာတော့ bucket name ကို kalaung-codedeploy လို့ သုံးပါမယ်။

  2. php-fpm.conf , production.nginx.conf ဖိုင်တွေကို s3 ပေါ် upload တင်ပါ။ သုံးရမဲ့ ဖိုင်တွေကို ဒီဆောင်းပါးရဲ့ အောက်ဆုံးမှာ စုပေးထားပါမယ်။ ကိုယ့် project ရဲ့ လိုအပ်ချက်ပေါ်မူတည်ပြီး လိုအပ်သလို ပြင်ပြီးမှ တင်ပါ။

  3. Project မှာ သုံးမဲ့ .env ဖိုင်ကို production.env လို့ နာမည်ပေးပြီး s3 ပေါ် upload တင်ပါ။

Setup IAM (policies, roles, user)

  1. IAM >> Policies ထဲမှာ Create policy ကို နှိပ်ပြီး ဒီ Policy လေးခုဆောက်ပါ။ Policy ထဲထည့်ရမဲ့ code တွေကို ဆောင်းပါးနဲ့ အောက်ဆုံးမှာ ယူလို့ ရပါတယ်။

    1. gitlab-ci-deploy-with-codedeploy

    2. gitlab-ci-manage-app-zip

    3. gitlab-ci-read-bucket

    4. gitlab-ci-autoscaling

  2. IAM >> Users ထဲမှာ Add user ကို နှိပ်ပြီး user အသစ်လုပ်ပါမယ်။ ဒီဆောင်းပါးမှာ user name ကို gitlab-ci-user လို့ သုံးပါမယ်။ Attach policies directly ကိုရွေးပြီးတော့ gitlab-ci-deploy-with-codedeploy နဲ့ gitlab-ci-manage-app-zip policy တွေ ကို ဒီ user မှာ attach တွဲပေးလိုက်ပါ။

  3. IAM >> Roles ထဲမှာ EC2 (Service Role) တစ်ခု ပြုလုပ်ပါ။ gitlab-ci-read-bucket policy ကို attach တွဲပေးပြီး Role name ကို gitlab-ci-ec2-role လို့ ပေးလိုက်ပါ။

  4. IAM >> Roles ထဲမှာ CodeDeploy (Service Role) တစ်ခု ပြုလုပ်ပါ။ AWSCodeDeployRole ဆိုတဲ့ default policy နဲ့ gitlab-ci-autoscaling policy ကို attach တွဲပေးပြီး Role name ကို gitlab-ci-codedeploy-role လို့ ပေးလိုက်ပါ။

Setup EC2

  1. EC2 Console ထဲသွားပြီးတော့ Application Load Balancer တစ်ခု ဆောက်ပါ။ ALB name ကို laravel-alb လို့ ပေးလိုက်ပါမယ်။ ALB ဆောက်တဲ့အချိန်မှာ Listeners and routing ထဲမှာ Default action အတွက် Target Group ပါ ဆောက်ဖို့ လိုပါလိမ့်မယ်။ Traget Group Name ကို LaravelTG လို့ ပေးလိုက်ပါမယ်။ TG ထဲမှာ Health check point ထည့်ခိုင်းတဲ့အခါမှာ ကိုယ့် app မှာ healthCheck point ပါရင် ထည့်ပေးပါ မပါရင်တော့ / ပဲ ထည့်လိုက်ပါ။ (Health Check point က ပါသင့်ပါတယ်။ ဒါမှ service တစ်ခုခု ဖြစ်တာနဲ့ ALB က တန်းသိနိုင်မှာပါ။ ကျွန်တော်ကတော့ HealthCheck အတွက် Laravel-health ကိုသုံးပါတယ်။)

  2. ပြီးသွားရင် EC2 Console မှာပဲ Launch Template လုပ်ပါမယ်။ Name ကို LaravelLT လို့ ပေးလိုက်ပါမယ်။

    • Name: LaravelLT

    • AMI: Amazon Linux 2 AMI

    • Advanced Detail >> IAM instance profile: gitlab-ci-ec2-role

    • Advanced Detail >> User data: ဒီဆောင်းပါးရဲ့ အောက်ဆုံးမှာ UserData ကိုရေးပေးထားပါမယ်။

    • ကျန်တဲ့ အချက်အလက်တွေကိုတော့ ကိုယ် လိုချင်သလို ရွေးချယ်လို့ရပါတယ်။ Instant ထဲကို ssh login ဝင်ဖို့ အစီအစဉ်ရှိရင်တော့ network မှာ interface ထည့်ပြီး public IP auto ပေးဖို့ ရွေးခဲ့ပါ။ Security Group လည်း တစ်ခုရွေးခဲ့ဖို့လိုပါတယ်။

  3. EC2 Consonsole ထဲမှာပဲ Auto Scaling Group တစ်ခု ဆောက်ပါမယ်။ Name ကိုတော့ LaravelASG လို့ ပေးလိုက်ပါမယ်။ Launch Template က LaravelLT ကိုရွေးပြီး။ ALB ကိုတော့ laravel-alb ကို ရွေးပေးလိုက်ပါ။ ဒီအဆင့်မှာ ကိုယ်သုံးချင်တဲ့ instance size တွေ storage size တွေ ရွေးပေးဖို့ လိုပါတယ်။ Auto Scaling Group ရဲ့ နောက်ဆုံးအဆင့် Tag ထဲမှာ Name tage create လုပ်ပြီး LaravelASG-instance လို့ပေးခဲ့လိုက်ပါ။ Instant တွေပြန်ကြည့်တဲ့အခါ ဒီ Group ထဲကမှန်း သိရတာပေါ့။

Creating CodeDeploy App and Group

  1. CodeDeploy console ထဲမှာ code deploy app အသစ် ဆောက်ပါမယ်။ name ကို LaravelCodeDeploy လို့ ပေးလိုက်ပါမယ်။ Compute platform မှာ EC2/On-premises ကိုရွေးပါ။

  2. LaravelCodeDeploy app ထဲမှာ codedeploy group ဆောက်ပါမယ်။ group name ကို LaravelCodeDeploy လို့ ပဲ ထပ်ပေးလိုက်ပါ။ Group ဆောက်တဲ့အခါမှာ။

    • Deployment Group Name: LaravelCodeDeploy

    • Service role: gitlab-ci-codedeploy-role

    • Deployment type: Blue/green

    • Environment configuration: Automatically copy Amazon EC2 Auto Scaling group

    • Amazon EC2 Auto Scaling group: LaravelASG

    • Load balancer: laravel-alb စတဲ့ အရှေ့ပိုင်းမှာ လုပ်ခဲ့တာတွေကို ပြန်ရွေးပေးရမှာ ဖြစ်ပါတယ်။

Setting Up in Gitlab repository

  1. git repository ထဲမှာ appspec.yml, devops/prepare.sh, devops/setup-app.sh နဲ့ .gitlab-ci.yml file တွေ ထည့်ပါမယ်။ ဒီဖိုင်တွေကို အောက်ဆုံးမှာ ဖေါ်ပြပေးထားပါတယ်။ ကိုယ့် project နဲ့ အဆင်ပြေသလို code တွေကို ပြင်သုံးဖို့ လိုပါမယ်။ အကုန်ထည့်ပြီးရင် gitlab ပေါ်ကို push ပေးထားပါ။

  2. ဒီ Environment variables တွေကို Gitlab repository ထဲ ထည့်ပေးပါ။

    1. AWS_ACCESS_KEY_ID: gitlab-ci-user ကနေ ထုတ်ရမှာပါ။

    2. AWS_SECRET_ACCESS_KEY: gitlab-ci-user ကနေ ထုတ်ရမှာပါ။

    3. AWS_REGION: အပေါ်က services တွေဆောက်ခဲ့တဲ့ region ကို ထည့်ပေးပါ။

    4. AWS_CODEDEPLOY_APP: LaravelCodeDeploy

    5. AWS_CODEDEPLOY_GROUP: LaravelCodeDeploy

    6. AWS_CODEDEPLOY_S3_BUCKET: kalaung-codedeploy

  3. ဒါတွေအကုန်ပြီးပြီဆိုရင်တော့ pipe line ကို စမ်းပြီး run ကြည့်လို့ရပါပြီ။

Gitlab Pipeline ထဲမှာ pass ဖြစ်သွားရင် CodeDeploy dashboard မှာ သွားကြည့်ပါ။ Deployment run နေတာကိုမြင်ရပါမယ်။

Deployment မှာ AllowTraffic အဆင့်က ကြာတတ်ပါတယ် အကုန်ပြီးသွားရင်တော့။ repo ထဲက latest app က autoscaling group ထဲမှာ runသွားပြီဖြစ်ပါတယ်။

Auto scaling group အသစ်မှာ run နေတာ error မရှိဘူးဆိုရင် နောက် တစ်နာရီကြာတဲ့အခါမှာ Auto scaling group အဟောင်းကို CodeDeploy က ဖျက်လိုက်ပါလိမ့်မယ်။ ဖျက်ဖို့ စောင့်တဲ့ အချိန်ကို ကိုယ့်စိတ်ကြိုက် သတ်မှတ်ပေးလို့ရပါတယ်။

CodeDeploy သုံးထားတဲ့ အားသာချက်က AutoScalingGroup မှာ capacity တိုးလာတာနဲ့မျှ တိုးလာတဲ့ Instance အသစ်တွေမှာ လက်ရှိ deployment version ကို CodeDeploy agent က တဆင့် auto deploy ပေးသွားမှာဖြစ်ပါတယ်။

Required configs and files

php-fpm.conf

[www]
user = apache
group = apache
listen = /run/php-fpm/www.sock
listen.acl_users = apache,nginx
listen.allowed_clients = 127.0.0.1
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
slowlog = /var/log/php-fpm/www-slow.log
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
php_admin_flag[log_errors] = on
php_value[session.save_handler] = files
php_value[session.save_path]    = /var/lib/php/session
php_value[soap.wsdl_cache_dir]  = /var/lib/php/wsdlcache

production.nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    include /etc/nginx/conf.d/*.conf;

    server {
        listen       80;
        listen       [::]:80;
        server_name  _;
        root         /var/www/app/public;

        include /etc/nginx/default.d/*.conf;

        index index.php index.html index.html
        disable_symlinks off;    

        add_header X-Frame-Options "SAMEORIGIN";
        add_header X-XSS-Protection "1; mode=block";
        add_header X-Content-Type-Options nosniff;
        add_header Allow "GET, POST, HEAD" always;

        proxy_connect_timeout       600;
        proxy_send_timeout          600;
        proxy_read_timeout          600;
        send_timeout                600;

        client_max_body_size 2048M;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        charset utf-8;

        location = /favicon.ico { access_log off; log_not_found off; }
        location = /robots.txt  { access_log off; log_not_found off; }

        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }

        location ~ \.php$ {
            fastcgi_pass unix:/run/php-fpm/www.sock;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
            include fastcgi_params;
        }

    }

}

gitlab-ci-deploy-with-codedeploy Policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "codedeploy:RegisterApplicationRevision",
                "codedeploy:CreateDeployment",
                "codedeploy:GetDeploymentConfig",
                "codedeploy:GetApplicationRevision"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

gitlab-ci-manage-app-zip Policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::kalaung-codedeploy/packages/*"
            ]
        }
    ]
}

gitlab-ci-read-bucket Policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:Get*",
                "s3:List*"
            ],
            "Resource": [
                "arn:aws:s3:::kalaung-codedeploy",
                "arn:aws:s3:::kalaung-codedeploy/*"
            ]
        }
    ]
}

gitlab-ci-autoscaling Policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iam:PassRole",
                "ec2:CreateTags",
                "ec2:RunInstances",
                "autoscaling:*"
            ],
            "Resource": "*"
        }
    ]
}

userdata to use in Launch Template

#!/bin/bash
yum update -y
yum install -y ruby wget curl zip unzip
yum install -y amazon-linux-extras
amazon-linux-extras enable php8.1
yum clean metadata
amazon-linux-extras install -y php8.1
yum install -y php-mbstring php-xml php-gd php-zip php-bcmath php-pgsql php-posix php-sodium
yum remove -y httpd
amazon-linux-extras install -y nginx1
amazon-linux-extras install -y epel

wget https://aws-codedeploy-ap-southeast-1.s3.amazonaws.com/latest/install
chmod +x install
./install auto

aws s3 cp s3://kalaung-codedeploy/production.nginx.conf /etc/nginx/nginx.conf
aws s3 cp s3://kalaung-codedeploy/php-fpm.conf /etc/php-fpm.d/www.conf 

systemctl enable nginx
systemctl enable php-fpm

systemctl start nginx
systemctl start php-fpm

usermod -a -G apache ec2-user

appspec.yml

# Definition file for AWS CodeDeploy

version: 0.0
os: linux
files:
  - source: /
    destination: /var/www/app
permissions:
  - object: /var/www/app
    owner: ec2-user
    group: apache
    type:
      - file
      - directory
hooks:
  BeforeInstall:
    - location: devops/prepare.sh
  AfterInstall:
    - location: devops/setup-app.sh

devops/prepare.sh

# Prepares for the deployment. Called from CodeDeploy's appspec.yml.

touch /tmp/deployment-started

# Download the .env file from S3 before emptying the directory to shave
# off a few seconds of downtime in case we don't deregister the instance
# from the load balancer.
aws s3 cp s3://kalaung-codedeploy/production.env /tmp/production.env
# aws s3 cp s3://kalaung-codedeploy/oauth-private.key /tmp/oauth-private.key
# aws s3 cp s3://kalaung-codedeploy/oauth-public.key /tmp/oauth-public.key

# Completely empty the app directory before dumping the revision's files
# there to avoid any deployment failures.
rm -Rf /var/www/app/
mkdir /var/www/app/
chown ec2-user:apache /var/www/app/

touch /tmp/deployment-cleared

devops/setup-app.sh

# Set up Laravel after main deployment. Called from CodeDeploy's
# appspec.yml.

# Move the previously downloaded .env file to the right place.
mv /tmp/production.env /var/www/app/.env
# mv /tmp/oauth-public.key /var/www/app/storage/oauth-public.key
# mv /tmp/oauth-private.key /var/www/app/storage/oauth-private.key

# Create the storage directories.
sudo mkdir -p /var/www/app/bootstrap/cache
sudo mkdir -p /var/www/app/storage/app/public
sudo mkdir -p /var/www/app/storage/logs
sudo mkdir -p /var/www/app/storage/framework/{sessions,views,cache}

# Run new migrations. While this is run on all instances, only the
# first execution will do anything. As long as we're using CodeDeploy's
# OneAtATime configuration we can't have a race condition.
sudo php /var/www/app/artisan migrate --force

# Run production optimizations.
sudo php /var/www/app/artisan config:cache
sudo php /var/www/app/artisan optimize
sudo php /var/www/app/artisan route:cache
sudo chown -R apache:apache /var/www/app/storage
sudo chown -R ec2-user:apache /var/www/app/bootstrap/cache
sudo rm -fr /var/www/app/.git

# Reload php-fpm to clear OPcache.
sudo systemctl restart php-fpm
sudo systemctl restart nginx

touch /tmp/deployment-done

.gitlab-ci.yml

stages:
  - deploy

image: lsfiege/laravel-php:8.1

before_script:
  - apt-get install -y wget curl zip
  - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
  - unzip awscliv2.zip
  - ./aws/install
  - rm -rf awscliv2.zip aws

production-deploy:
  stage: deploy
  script:
    - cp .env.example .env
    - composer install --no-interaction --quiet --no-scripts --prefer-dist
    - aws configure set default.region ${AWS_REGION}
    - aws deploy push --application-name ${AWS_CODEDEPLOY_APP} --s3-location s3://${AWS_CODEDEPLOY_S3_BUCKET}/packages/${CI_COMMIT_SHA}.zip --ignore-hidden-files
    - aws deploy create-deployment --application-name ${AWS_CODEDEPLOY_APP} --s3-location bucket=${AWS_CODEDEPLOY_S3_BUCKET},key=packages/${CI_COMMIT_SHA}.zip,bundleType=zip --deployment-group-name ${AWS_CODEDEPLOY_GROUP}
  when: manual
  only:
    - master