CircleCI Beanstalk Deployment example

  • November 24, 2024
  • 49

Hôm nay chúng ta sẽ tìm hiểu các số khái niệm cần thiết khi deploy dựa án cakephp sử dụng circleCI aws elastic beastalk. Cùng xem file config circleCI sau và giải thích các config khi deploy dự án.


version: 2.1

orbs:
  aws-cli: circleci/aws-cli@4.1.3
  aws-ecr: circleci/aws-ecr@9.1.0
  eb: circleci/aws-elastic-beanstalk@2.0.1
  node: circleci/node@5.2.0
  php: circleci/php@1.1.0

commands:
  setup-aws-credentials-dev:
    steps:
      - aws-cli/setup:
          aws_access_key_id: $AWS_ACCESS_KEY_ID
          aws_secret_access_key: $AWS_SECRET_ACCESS_KEY
          region: ap-southeast-1
      - run:
          name: AWS credentials export environment variables
          command: |
            echo 'export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID' >> $BASH_ENV
            echo 'export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY' >> $BASH_ENV
            echo 'export AWS_DEFAULT_REGION=ap-southeast-1' >> $BASH_ENV

  setup-aws-credentials-prod:
    steps:
      - aws-cli/setup:
          aws_access_key_id: $PROD_AWS_ACCESS_KEY_ID
          aws_secret_access_key: $PROD_AWS_SECRET_ACCESS_KEY
          region: ap-southeast-1
      - run:
          name: AWS credentials export environment variables
          command: |
            echo 'export AWS_ACCESS_KEY_ID=$PROD_AWS_ACCESS_KEY_ID' >> $BASH_ENV
            echo 'export AWS_SECRET_ACCESS_KEY=$PROD_AWS_SECRET_ACCESS_KEY' >> $BASH_ENV
            echo 'export AWS_DEFAULT_REGION=ap-southeast-1' >> $BASH_ENV 

  copy-cake-env-file:
    parameters:
      env:
        type: string
    steps:
      - run:
          name: Set .env from file << parameters.env >>
          working_directory: ./app/config
          command: |
            cp -f .env.<< parameters.env >> .env
            if [ << parameters.env >> == "circleci" ]
            then
              source .env
            else
              echo .env.<< parameters.env >>
            fi
            ls -all

  check-build-status:
    steps:
      - run:
          name: check build status of master branch
          command: |
            api_url=https://circleci.com/api/v1.1/project/github/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/tree/master
            while
              result=$(curl -H "Circle-Token: ${CIRCLE_TOKEN}" $api_url | jq -r 'unique_by(.workflows.job_name) | .[] | [.workflows.job_name, .lifecycle, .outcome] | @tsv')
              echo "$result"
              # 1 個以上の全てが finished になるまで
              echo "$result" | fgrep -q -v finished
            do
              sleep 60
            done
            if echo "$result" | fgrep -q failed; then exit 1; fi

  build-and-push-docker-image:
    parameters:
      region:
        type: string
        default: ap-southeast-1
      repo:
        type: string
        default: coincome
      tag:
        type: string
        default: latest
      account_id:
        type: string
      auth:
        type: steps
        default: []
    steps:
      - aws-ecr/build_and_push_image:
          auth: << parameters.auth >>
          account_id: << parameters.account_id >>
          build_path: ./docker/phpfpm
          path: ./docker/phpfpm
          extra_build_args: "--target prod"
          repo: << parameters.repo >>
          tag: << parameters.tag >>
          region: << parameters.region >>
      - steps: << parameters.auth >>
      - run:
          name: Remove untaged images
          command: |
            # tag を push すると、すでに存在する tag が untagged になり、ECRに残り続けるので削除する
            IMAGE_IDS=$(aws ecr list-images --region << parameters.region >> --repository-name << parameters.repo >> --filter "tagStatus=UNTAGGED" --query 'imageIds[*]' --output json --max-items 100)
            aws ecr batch-delete-image --region << parameters.region >> --repository-name << parameters.repo >> --image-ids "$IMAGE_IDS" || true
      # - run:
      #     name: Test image
      #     working_directory: ~/project/docker
      #     command: |
      #       docker run [ecr_image] php -v
      #       echo
      #       docker run [ecr_image] supervisord -v

jobs:
  build-and-push-docker-image-dev:
    parameters:
      tag:
        type: string
    executor:
      name: aws-ecr/default
    steps:
      - build-and-push-docker-image:
          region: "ap-southeast-1"
          repo: "coincome"
          tag: << parameters.tag >>
          account_id: "211699566303"
          auth:
            - setup-aws-credentials-dev

  build-webpack:
    executor:
      name: node/default
      tag: "12.19"
    steps:
      - checkout
      - node/install-packages:
          cache-path: ~/project/node_modules
          override-ci-command: npm install
      - run:
          name: Build webpack
          command: npm run dev
      - persist_to_workspace:
          root: ~/project
          paths:
            - app/webroot

  check-build-status-master:
    docker:
      - image: cimg/base:stable
    steps:
      - check-build-status

  build-php:
    docker:
      - image: cimg/php:8.2
    steps:
      - checkout
      - php/install-composer
      - php/install-packages:
          app-dir: app
          with-cache: true
      - persist_to_workspace:
          root: ~/project
          paths:
            - app/vendor

  build-php-subfolder:
    docker:
      - image: cimg/php:8.2-node
    steps:
      - checkout
      - php/install-composer
      - run:
          name: Pre installe required tools
          command: |
            sudo apt-get update -y
            sudo apt-get install -y libicu-dev libmemcached-dev
            echo "/usr" | sudo pecl install memcached
            echo "extension=memcached.so" | sudo tee /usr/local/etc/php/conf.d/memcached.ini
            sudo docker-php-ext-install >/dev/null -j$(nproc) gettext
      - php/install-packages:
          app-dir: app2
          with-cache: true
      - persist_to_workspace:
          root: ~/project
          paths:
            - app2/vendor

  test:
    docker:
      - image: cimg/php:8.2
      - image: cimg/mysql:8.0
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: true
          MYSQL_DATABASE: "test_circleci_db"
          MYSQL_USER: ubuntu
          MYSQL_PASSWORD: password
        command: ["--sql-mode="]
    resource_class: medium+
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: true
      - attach_workspace:
          at: ~/project
      - run:
          name: Waiting for MySQL to be ready
          command: dockerize -wait tcp://127.0.0.1:3306 -timeout 30s
      - copy-cake-env-file:
          env: circleci
      - run:
          name: Make sure folder vender and webroot existed
          command: |
            cp -r ./app/config/app.default.php ./app/config/app.php
      - run:
          name: Migration data for test
          working_directory: app
          command: |
            bin/cake migrations migrate
            bin/cake migrations migrate -p LoginAttempts
            bin/cake migrations migrate -p Queue
            php bin/cake.php schema_cache clear
      - run:
          working_directory: app
          name: Run tests
          command: composer test
      - run:
          name: composer po-check
          command: composer po-check -- --ansi
          working_directory: app

  deploy:
    executor:
      name: aws-cli/default
    parameters:
      aws-account-id:
        type: string
      environment-name:
        type: string
      application-name:
        type: string
      region:
        type: string
        default: ap-southeast-1
      repo:
        type: string
      is-web-server:
        type: boolean
      tag:
        type: string
        default: latest
      app-env:
        type: string
      auth:
        type: steps
        default: []
    steps:
      - checkout
      - attach_workspace:
          at: ~/project
      - steps: << parameters.auth >>
      - run:
          name: Update to application name "<< parameters.application-name >>" .elasticbeanstalk/config.global.yml
          command: |
            cp -af .elasticbeanstalk/config.global.org.yml .elasticbeanstalk/config.global.yml
            sed -i -e 's/APPLICATIONNAME/<< parameters.application-name >>/' .elasticbeanstalk/config.global.yml
            sed -i -e 's/APPLICATION_REGION/<< parameters.region >>/' .elasticbeanstalk/config.global.yml
            cat .elasticbeanstalk/config.global.yml
      - run:
          name: Update << parameters.application-name >> for 02.migration.config
          command: |
            env="<< parameters.environment-name >>"
            if [[ $env == *"worker"* ]]; then
              cp -af deploy/Dockerrun.worker.json Dockerrun.aws.json
            elif [[ $env == *"prod"* ]]; then
              cp -af deploy/Dockerrun.prod.json Dockerrun.aws.json
            else
              cp -af deploy/Dockerrun.dev.json Dockerrun.aws.json
            fi
            sed -i -e 's/DOCKER_IMAGE_NAME/<< parameters.repo >>/' .platform/hooks/postdeploy/02_migrations.sh
            sed -i -e 's/DOCKER_IMAGE_VERSION/<< parameters.tag >>/' .platform/hooks/postdeploy/02_migrations.sh
            sed -i -e 's/AWS_ACCOUNT_ID/<< parameters.aws-account-id >>/' .platform/hooks/postdeploy/02_migrations.sh
            sed -i -e 's/AWS_DEFAULT_REGION/<< parameters.region >>/' .platform/hooks/postdeploy/02_migrations.sh
            chmod +x .platform/hooks/postdeploy/02_migrations.sh
            sed -i -e 's/DOCKER_IMAGE_NAME/<< parameters.repo >>/' .ebextensions/04.crontab.config
            sed -i -e 's/DOCKER_IMAGE_VERSION/<< parameters.tag >>/' .ebextensions/04.crontab.config
            sed -i -e 's/AWS_ACCOUNT_ID/<< parameters.aws-account-id >>/' .ebextensions/04.crontab.config
            sed -i -e 's/AWS_DEFAULT_REGION/<< parameters.region >>/' .ebextensions/04.crontab.config
            sed -i -e 's/DOCKER_IMAGE_NAME/<< parameters.repo >>/' Dockerrun.aws.json
            sed -i -e 's/DOCKER_IMAGE_VERSION/<< parameters.tag >>/' Dockerrun.aws.json
            sed -i -e 's/AWS_ACCOUNT_ID/<< parameters.aws-account-id >>/' Dockerrun.aws.json
            sed -i -e 's/AWS_DEFAULT_REGION/<< parameters.region >>/' Dockerrun.aws.json
      - run:
          name: Init ECS task (Dockerrun.aws.json) update memory
          command: |
            # Get EB instance type
            instanceType=$(aws ec2 describe-instances --filters "Name=tag:elasticbeanstalk:environment-name,Values=<< parameters.environment-name >>" --query "Reservations[0].Instances[0].InstanceType" --output text)
            # Get memory info of instance type
            instanceMemory=$(curl -s https://tyrell-aws.s3-ap-northeast-1.amazonaws.com/ec2.json | jq --arg instanceType $instanceType '.[] | select (.InstanceType==$instanceType) | .Memory | tonumber')
            if [ -z "$instanceMemory" ]; then
              # exit if memory not found
              exit 1
            fi
            # GiB * 1024 = MiB , assign 70% memory
            MEMORY=$(echo $instanceMemory | awk '{printf ("%.0f", $1 * 1024 * .7)}')
            cat Dockerrun.aws.json \
              | jq --arg memory $MEMORY \
              '(.containerDefinitions[] | select(.name == "php-app" or .name == "app-worker") | .memory) |= ($memory | tonumber)' \
              > Dockerrun.aws.json.new && \
              mv -f Dockerrun.aws.json.new Dockerrun.aws.json
      - copy-cake-env-file:
          env: << parameters.app-env >>
      - run:
          name: Show Dockerrun.aws.json
          command: |
            cat Dockerrun.aws.json
      - run:
          name: Make sure folder vender and webroot existed
          command: |
            cp -r ~/project/app/config/app.default.php ~/project/app/config/app.php
     
      
      - run:
          name: Create assetlinks files
          command: |
            env="<< parameters.environment-name >>"
            if [[ $env == *"dev"* ]]; then
              cp -af app/webroot/.well-known/assetlinks.json.dev app/webroot/.well-known/assetlinks.json
              cp -af app/webroot/apple-app-site-association.dev app/webroot/apple-app-site-association
            elif [[ $env == *"stg"* ]]; then
              cp -af app/webroot/.well-known/assetlinks.json.stg app/webroot/.well-known/assetlinks.json
              cp -af app/webroot/apple-app-site-association.stg app/webroot/apple-app-site-association
            else
              cp -af app/webroot/.well-known/assetlinks.json.prod app/webroot/.well-known/assetlinks.json
              cp -af app/webroot/apple-app-site-association.prod app/webroot/apple-app-site-association
            fi
      - unless:
          condition: <>
          steps:
            - run: rm .ebextensions/00.network-load-balancer.config
      - run:
          name: Remove migrations script if not worker environment
          command: |
            env="<< parameters.environment-name >>"
            if [[ $env == *"worker"* ]]; then
              echo "OK"
            else
              rm -f .platform/hooks/postdeploy/02_migrations.sh
            fi
      - eb/setup
      - run:
          name: Deploy with elastic beanstalk << parameters.environment-name >>, estimate time 5-10 minutes.
          command: eb deploy --label "<< parameters.environment-name >>-<< pipeline.number >>" "<< parameters.environment-name >>" --debug

filters-branch-develop: &filters-branch-develop
  branches:
    only:
      - develop

filters-branch-staging: &filters-branch-staging
  branches:
    only:
      - master

filter_tags_release: &filter_tags_release
  tags:
    only: /^v[0-9]+(\.[0-9]+)$/
  branches:
    ignore: /.*/

filter_branch_other: &filter_branch_other
  tags:
    ignore: /^v[0-9]+(\.[0-9]+)$/
  branches:
    ignore:
      - master
      - develop

workflows:
  version: 2

  test:
    jobs:
      - build-webpack:
          filters: *filter_branch_other
      - build-php:
          filters: *filter_branch_other
      - test:
          filters: *filter_branch_other
          requires:
            - build-php

  deploy-with-build-and-test-develop:
    jobs:
      - build-webpack:
          filters: *filters-branch-develop
      - build-php:
          filters: *filters-branch-develop
      - test:
          filters: *filters-branch-develop
          requires:
            - build-php
      - build-and-push-docker-image-dev:
          tag: develop
          requires:
            - build-webpack
            - build-php
          filters: *filters-branch-develop
      - deploy:
          auth:
            - setup-aws-credentials-dev
          name: deploy_develop_web
          is-web-server: true
          tag: "develop"
          environment-name: "demo-dev-al2023"
          app-env: "dev"
          aws-account-id: [your_account_id]
          repo: "app"
          application-name: "app-demo"
          requires:
            - build-and-push-docker-image-dev
          filters: *filters-branch-develop
      - deploy:
          auth:
            - setup-aws-credentials-dev
          name: deploy_develop_worker
          is-web-server: false
          tag: "develop"
          environment-name: "demo-al2023-worker"
          app-env: "worker-dev"
          aws-account-id: [your_account_id]
          repo: "app"
          application-name: "app-demo"
          requires:
            - build-and-push-docker-image-dev
          filters: *filters-branch-develop

  deploy-with-build-and-test-staging:
    jobs:
      - build-webpack:
          filters: *filters-branch-staging
      - build-php:
          filters: *filters-branch-staging
      - test:
          filters: *filters-branch-staging
          requires:
            - build-php
      - build-and-push-docker-image-dev:
          tag: latest
          requires:
            - build-webpack
            - test
          filters: *filters-branch-staging
      - deploy:
          auth:
            - setup-aws-credentials-dev
          name: deploy_staging_web
          is-web-server: true
          tag: "develop"
          environment-name: "demo-stg-al2023"
          app-env: "stg"
          aws-account-id: [your_account_id]
          repo: "app"
          application-name: "app-demo"
          requires:
            - deploy_staging_worker
          filters: *filters-branch-staging
      - deploy:
          auth:
            - setup-aws-credentials-dev
          name: deploy_staging_worker
          is-web-server: false
          tag: "develop"
          environment-name: "demo-stg-al2023-worker"
          app-env: "worker-stg"
          aws-account-id: [your_account_id]
          repo: "app"
          application-name: "app-demo"
          requires:
            - build-and-push-docker-image-dev
          filters: *filters-branch-staging
  deploy-prod:
    jobs:
      - build-webpack:
          filters: *filter_tags_release
      - build-php:
          filters: *filter_tags_release
      - check-build-status-master:
          filters: *filter_tags_release
      - deploy:
          auth:
            - setup-aws-credentials-prod
          name: deploy_prod_web
          is-web-server: true
          tag: latest
          environment-name: "demo-prod-al2023"
          app-env: "prod"
          aws-account-id: [your_account_id]
          repo: "app"
          application-name: "app-demo"
          requires:
            - deploy_prod_worker
          filters: *filter_tags_release
      - deploy:
          auth:
            - setup-aws-credentials-prod
          name: deploy_prod_worker
          is-web-server: false
          tag: latest
          environment-name: "demo-prod-al2023-worker"
          app-env: "worker-prod"
          aws-account-id: [your_account_id]
          repo: "app"
          application-name: "app-demo"
          requires:
            - build-webpack
            - build-php
            - check-build-status-master
          filters: *filter_tags_release
  

1. Workflows

Định nghĩa các công việc chính cần làm, như test một nhánh, deploy code lên môi trường dev, stg, prod. Trong workflow sẽ đinh nghĩa các công việc cần làm(jobs), định nghĩa jobs chạy song song hay tuần tự, và điều kiện khi nào workflow hoạt động. VD: Workflows test sẽ chạy 2 jobs build-webpack và build-php song song, khi build xong job build-php mới thực hiện job test, điểu kiện để chạy workflow test là các nhánh không phải develop, staging và tag release.

2. Job

Định nghĩa các bước làm 1 công việc cụ thể, đinh nghĩa môi trường thực thi, sử dụng các orbs được viết sẵn, định nghĩa các tham số cho command. VD: job build-php Định nghĩa các công việc để build một môi trường php 8.2, các bước build môi trường php 8.2 checkout code, cài đặt composer, cài đặt các package với composer.

3. Command

Có thể hiểu như một function được sửa dụng trong jobs, 1 command bao gồm các step và có thể truyền được tham số vào từ jobs. Viết command giúp định nghĩa job không bị lặp code.

4. orbs

Orbs là các packages hay được sửa dụng, dùng job sẽ tiết kiệm thời gian định nghĩa job. Do các công việc hay làm đã được đóng gọi sẵn, chỉ việc dùng orbs không cần phải viết lại code dài dòng

5. filters

Dùng để định nghĩa điều kiện khi nào workflow được chạy. VD: filters-branch-develop Đinh nghĩa khi code được merge vào branch develop sẽ thoả mãn điều kiện filter.

Tổng kết

Như vậy là chúng ta đã tìm hiểu xong các bước cơ bản để deploy code sử dụng circleCI. Thanks for reading...

Tài liệu tham khảo

https://circleci.com/docs/deploy-to-aws/ https://circleci.com/docs/concepts/ https://kevingoedecke.com/2018/03/12/circleci-2-0-beanstalk-example-tutorial/