From 0c83bd16c027901b57fc50f16e514cb7c1372907 Mon Sep 17 00:00:00 2001 From: iknowright Date: Mon, 27 Feb 2023 22:10:05 +0800 Subject: [PATCH 1/9] add cd --- .github/workflows/cd.yml | 29 +++++++++++++++++++++++++++++ deploy.yml | 15 +++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .github/workflows/cd.yml create mode 100644 deploy.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 000000000..9a487a970 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,29 @@ +name: CD + +on: [workflow_dispatch, pull_request, push] + +jobs: + cd: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Decode private key file + run: | + echo "${{secrets.SSH_PRIVATE_KEY}}" | base64 --decode > "private.pem" + chmod 400 private.pem + + - name: Run playbook + uses: dawidd6/action-ansible-playbook@v2 + with: + playbook: deploy.yml + inventory: | + pycontw: + hosts: + staging: + ansible_host: staging.pycon.tw + ansible_user: changchaishi + ansible_ssh_private_key_file: private.pem + ansible_python_interpreter: /usr/bin/python3 + diff --git a/deploy.yml b/deploy.yml new file mode 100644 index 000000000..75c3bf31a --- /dev/null +++ b/deploy.yml @@ -0,0 +1,15 @@ +--- +- name: Check services + hosts: staging + + tasks: + - name: Ensure that sshd is started + community.general.python_requirements_info: + dependencies: + - docker + + - name: Get info on docker host and list images + community.docker.docker_host_info: + images: true + become: true + register: result From 263043dc0bb532c56798c0593e759a534cfabe85 Mon Sep 17 00:00:00 2001 From: iknowright Date: Tue, 28 Feb 2023 13:37:53 +0800 Subject: [PATCH 2/9] update playbook to build docker-image --- .github/workflows/cd.yml | 4 ++-- deploy.yml | 34 ++++++++++++++++++++++++++++------ docker-compose.yml | 3 ++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9a487a970..518c046c2 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -11,10 +11,11 @@ jobs: - name: Decode private key file run: | + echo "${{secrets.PRODUCTION_DOT_ENV_FILE}}" > .env echo "${{secrets.SSH_PRIVATE_KEY}}" | base64 --decode > "private.pem" chmod 400 private.pem - - name: Run playbook + - name: Run CD playbook uses: dawidd6/action-ansible-playbook@v2 with: playbook: deploy.yml @@ -26,4 +27,3 @@ jobs: ansible_user: changchaishi ansible_ssh_private_key_file: private.pem ansible_python_interpreter: /usr/bin/python3 - diff --git a/deploy.yml b/deploy.yml index 75c3bf31a..1ff2ffac4 100644 --- a/deploy.yml +++ b/deploy.yml @@ -1,15 +1,37 @@ --- - name: Check services hosts: staging + # need to use become since I'm connecting using personal private key + become: true + # switch user as dev + become_user: dev + vars: + project_dir: /home/dev/web-projects/pycontw-2023-ansible tasks: - - name: Ensure that sshd is started + - name: Ensure that Docker for python is present (docker in pip) community.general.python_requirements_info: dependencies: - docker - - name: Get info on docker host and list images - community.docker.docker_host_info: - images: true - become: true - register: result + - name: Create a directory if it does not exist + ansible.builtin.file: + path: "{{ project_dir }}" + state: directory + + - name: Copy entire project files to remote server + ansible.builtin.copy: + src: . + dest: "{{ project_dir }}" + + - name: Ensure docker network network-2023 exists + community.docker.docker_network: + name: network-2023 + + - name: Build and start service + community.docker.docker_compose: + project_src: web-projects/pycontw-2023-ansible + build: true + # try to build first, without up the service + state: present + diff --git a/docker-compose.yml b/docker-compose.yml index 7752e978e..11448720b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,8 @@ version: "3.5" services: web: build: . - container_name: pycontw-2023 + container_name: pycontw-2023-ansible + image: pycontw-2023_web-ansible hostname: pycontw-2023 entrypoint: "" command: From a0f03a1aaab0e63a21390f97a0fc8413e15bfacc Mon Sep 17 00:00:00 2001 From: iknowright Date: Tue, 28 Feb 2023 15:21:11 +0800 Subject: [PATCH 3/9] use synchronize instead of copy --- .github/workflows/cd.yml | 2 +- deploy.yml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 518c046c2..de0bf5585 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -26,4 +26,4 @@ jobs: ansible_host: staging.pycon.tw ansible_user: changchaishi ansible_ssh_private_key_file: private.pem - ansible_python_interpreter: /usr/bin/python3 + ansible_python_interpreter: /home/dev/.pyenv/shims/python diff --git a/deploy.yml b/deploy.yml index 1ff2ffac4..1401fa704 100644 --- a/deploy.yml +++ b/deploy.yml @@ -20,9 +20,10 @@ state: directory - name: Copy entire project files to remote server - ansible.builtin.copy: - src: . + ansible.posix.synchronize: + src: ./ dest: "{{ project_dir }}" + delete: true - name: Ensure docker network network-2023 exists community.docker.docker_network: @@ -30,8 +31,7 @@ - name: Build and start service community.docker.docker_compose: - project_src: web-projects/pycontw-2023-ansible + project_src: "{{ project_dir }}" build: true # try to build first, without up the service - state: present - + state: absent From f2739531f5ca54ead07aef0f957e5d7e5bdddb0a Mon Sep 17 00:00:00 2001 From: iknowright Date: Tue, 7 Mar 2023 22:29:12 +0800 Subject: [PATCH 4/9] update staging files --- .github/workflows/cd.yml | 22 +++++++++++++----- deploy.yml | 16 ++++++------- docker-compose-staging.yml | 47 ++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 3 +-- 4 files changed, 72 insertions(+), 16 deletions(-) create mode 100644 docker-compose-staging.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index de0bf5585..c6614a6ae 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,21 +1,30 @@ name: CD -on: [workflow_dispatch, pull_request, push] +on: + workflow_dispatch: + push: + branches: + - 'master' jobs: cd: + if: | + github.event_name == 'push' || ( + github.event_name == 'workflow_dispatch' && + contains(fromJSON(vars.PROJECT_ADMINS), github.actor) + ) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - - - name: Decode private key file + - name: Generate .env for staging vm from github secrets run: | echo "${{secrets.PRODUCTION_DOT_ENV_FILE}}" > .env + - name: Decode private key file for OpenSSH access over Ansible + run: | echo "${{secrets.SSH_PRIVATE_KEY}}" | base64 --decode > "private.pem" chmod 400 private.pem - - - name: Run CD playbook + - name: Run playbook for deployment uses: dawidd6/action-ansible-playbook@v2 with: playbook: deploy.yml @@ -24,6 +33,7 @@ jobs: hosts: staging: ansible_host: staging.pycon.tw - ansible_user: changchaishi + ansible_user: "${{secrets.GCE_USERNAME}}" + # secret file generated from previous step ansible_ssh_private_key_file: private.pem ansible_python_interpreter: /home/dev/.pyenv/shims/python diff --git a/deploy.yml b/deploy.yml index 1401fa704..f1cb9faed 100644 --- a/deploy.yml +++ b/deploy.yml @@ -1,25 +1,26 @@ --- -- name: Check services +- name: Deploy project to staging machine hosts: staging - # need to use become since I'm connecting using personal private key + # escalate privilege become: true - # switch user as dev become_user: dev vars: project_dir: /home/dev/web-projects/pycontw-2023-ansible tasks: - - name: Ensure that Docker for python is present (docker in pip) + - name: Dependencies check dor docker and docker-compose in remote server community.general.python_requirements_info: dependencies: - docker + - docker-compose - - name: Create a directory if it does not exist + - name: Create project directory (if not exist) ansible.builtin.file: path: "{{ project_dir }}" state: directory - - name: Copy entire project files to remote server + # Copy project files to remote server (.env is included) + - name: Copy project files to remote server ansible.posix.synchronize: src: ./ dest: "{{ project_dir }}" @@ -33,5 +34,4 @@ community.docker.docker_compose: project_src: "{{ project_dir }}" build: true - # try to build first, without up the service - state: absent + state: present diff --git a/docker-compose-staging.yml b/docker-compose-staging.yml new file mode 100644 index 000000000..11448720b --- /dev/null +++ b/docker-compose-staging.yml @@ -0,0 +1,47 @@ +version: "3.5" +services: + web: + build: . + container_name: pycontw-2023-ansible + image: pycontw-2023_web-ansible + hostname: pycontw-2023 + entrypoint: "" + command: + # Hacky script for quick demonstration purpose + - bash + - -c + - | + set -o errexit -o nounset -o pipefail + python3 manage.py compilemessages + python3 manage.py migrate + python3 manage.py collectstatic --no-input + + exec uwsgi --http-socket :8000 \ + --master \ + --hook-master-start "unix_signal:15 gracefully_kill_them_all" \ + --static-map /static=assets \ + --static-map /media=media \ + --mount /prs=pycontw2016/wsgi.py \ + --manage-script-name \ + --offload-threads 2 + restart: always + environment: + # Save us from having to type `--setting=pycontw2016.settings.production` + DJANGO_SETTINGS_MODULE: pycontw2016.settings.production.pycontw2023 + SCRIPT_NAME: /prs + SECRET_KEY: ${SECRET_KEY} + DATABASE_URL: ${DATABASE_URL} + EMAIL_URL: ${EMAIL_URL} + DSN_URL: ${DSN_URL} + GTM_TRACK_ID: ${GTM_TRACK_ID} + SLACK_WEBHOOK_URL: ${SLACK_WEBHOOK_URL} + + volumes: + - ${MEDIA_ROOT}:/usr/local/app/src/media + networks: + - network + +networks: + network: + external: true + name: network-2023 diff --git a/docker-compose.yml b/docker-compose.yml index 11448720b..7752e978e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,7 @@ version: "3.5" services: web: build: . - container_name: pycontw-2023-ansible - image: pycontw-2023_web-ansible + container_name: pycontw-2023 hostname: pycontw-2023 entrypoint: "" command: From e42879fd25b1827ce6d10da544094e9fbbca361f Mon Sep 17 00:00:00 2001 From: iknowright Date: Wed, 8 Mar 2023 00:18:38 +0800 Subject: [PATCH 5/9] add documentation for CD pipeline --- README.md | 3 ++ document/continuous_deployment.md | 46 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 document/continuous_deployment.md diff --git a/README.md b/README.md index 14676d65e..f05554e40 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,6 @@ We strongly recommend you configure your editor to match our coding styles. You ## Deployment For site administrators, please refer to [document/deploy_docker_prod.md](/document/deploy_docker_prod.md). + +### Continuous Deployment +Currently this is only for continuous deployment on staging server, please refer to [document/continuous_deployment.md](/document/continuous_deployment.md). diff --git a/document/continuous_deployment.md b/document/continuous_deployment.md new file mode 100644 index 000000000..28ac26a4b --- /dev/null +++ b/document/continuous_deployment.md @@ -0,0 +1,46 @@ +# Continuous Deployment on Staging Server + +The following describes how to setup continuous deployment for staging server. This setup presumes the site administrators have site deployment practices based on the docker production deployment [document/deploy_docker_prod.md](/document/deploy_docker_prod.md). + +## Requirements for Staging Server +The staging server should have the following installed: +- Docker 17.09+ (since we use `--chown` flag in the COPY directive) +- Docker Compose +- python3.6+ +- [docker](https://pypi.org/project/docker/) SDK for python +- [docker-compose](https://pypi.org/project/docker-compose/) SDK for python + + +## Prerequisite for Site Administrators +- Gather Container Environment Variables as mention in [document/deploy_docker_prod.md](/document/deploy_docker_prod.md). +- Have a ssh user and secret file for accessing GCE instance (staging machine) + - Secret file will be further encoded by base64 +- Administrators github Ids + - For CD workflow authorization + +## Settings for Github Actions Workflow +After aboves steps, we have to add collected information to github actions setting. +Please configure as the following in project's setting: + +| Level | Type | Name | Value (example) | Remarks | +|-----------|------------|---------------|----------|------------| +| Repository | secrets | PRODUCTION_DOT_ENV_FILE | `DATABASE_URL=...` | multiline support | +| Repository | secrets | GCE_USERNAME | cd_user | user name for ssh {user_name}@staging.pycon.tw | +| Repository | secrets | SSH_PRIVATE_KEY | `21xa312....` | base64 encoded of key-pair (`.pem` file) | +| Repository | variables | PROJECT_ADMINS | `["github_user_1", "github_user_2"]` | For example `["josix"]` | + +Reference +- [Create a secret for a repository](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) +- [Create a variable for a repository](https://docs.github.com/en/actions/learn-github-actions/variables#creating-configuration-variables-for-a-repository) +- Create base64 encoded string for `key.pem` + - `base64 -i key.pem` (mac) + - `cat key.pem | base64` (linux) + +## Review +### Events that triggers the pipeline +1. When the PR merges to `master` + - no authorization needed, as PRs normally reviewed before merge +2. Manually [trigger](https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow#running-a-workflow) the CD workflow (By admins) + - only for Administrator specify in repository's variable called *PROJECTS_ADMINS* + +Why? CD workflow will directly access to the GCE instance, should prevent unwanted deployments from PRs or push. (As a deployment guardian) From e166efc461bdf8d884a547554cd9ad5fd1867889 Mon Sep 17 00:00:00 2001 From: iknowright Date: Wed, 8 Mar 2023 21:49:37 +0800 Subject: [PATCH 6/9] rename compose file --- deploy.yml | 3 +++ docker-compose-staging.yml => docker-compose-ansible.yml | 0 2 files changed, 3 insertions(+) rename docker-compose-staging.yml => docker-compose-ansible.yml (100%) diff --git a/deploy.yml b/deploy.yml index f1cb9faed..5fb9956e6 100644 --- a/deploy.yml +++ b/deploy.yml @@ -33,5 +33,8 @@ - name: Build and start service community.docker.docker_compose: project_src: "{{ project_dir }}" + files: + # use ansible-specific compose file + - docker-compose-ansible.yml build: true state: present diff --git a/docker-compose-staging.yml b/docker-compose-ansible.yml similarity index 100% rename from docker-compose-staging.yml rename to docker-compose-ansible.yml From f874de81c43eb2fe90d24fa27d063c45a3de681c Mon Sep 17 00:00:00 2001 From: iknowright Date: Wed, 8 Mar 2023 21:59:57 +0800 Subject: [PATCH 7/9] make cd server flexible --- .github/workflows/cd.yml | 6 +++--- document/continuous_deployment.md | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index c6614a6ae..9d570f502 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -32,8 +32,8 @@ jobs: pycontw: hosts: staging: - ansible_host: staging.pycon.tw - ansible_user: "${{secrets.GCE_USERNAME}}" + ansible_host: "${{secrets.VM_DOMAIN_IP}}" + ansible_user: "${{secrets.VM_USERNAME}}" # secret file generated from previous step ansible_ssh_private_key_file: private.pem - ansible_python_interpreter: /home/dev/.pyenv/shims/python + ansible_python_interpreter: "${{secrets.VM_PYTHON_INTERPRETER}}" diff --git a/document/continuous_deployment.md b/document/continuous_deployment.md index 28ac26a4b..175ab5724 100644 --- a/document/continuous_deployment.md +++ b/document/continuous_deployment.md @@ -19,13 +19,18 @@ The staging server should have the following installed: - For CD workflow authorization ## Settings for Github Actions Workflow -After aboves steps, we have to add collected information to github actions setting. -Please configure as the following in project's setting: +After aboves steps, we have to add collected information to Github actions setting. + +Under the hood, we github action and [Ansible](https://www.ansible.com/overview/how-ansible-works) for continuous deployment. Github action will hold necessary variables and secrets that allows Ansible to access the staging VM on your behalf. + +So kindly configure project's action setting as the following: | Level | Type | Name | Value (example) | Remarks | |-----------|------------|---------------|----------|------------| | Repository | secrets | PRODUCTION_DOT_ENV_FILE | `DATABASE_URL=...` | multiline support | -| Repository | secrets | GCE_USERNAME | cd_user | user name for ssh {user_name}@staging.pycon.tw | +| Repository | secrets | VM_USERNAME | cd_user | user name for ssh {user_name}@{vm_domain} | +| Repository | secrets | VM_DOMAIN_IP | staging.pycon.tw | IP address or Domain that points to the staging server | +| Repository | secrets | VM_PYTHON_INTERPRETER | `/home/dev/.pyenv/shims/python` | path to your python environment that has docker/docker-compose packages installed | | Repository | secrets | SSH_PRIVATE_KEY | `21xa312....` | base64 encoded of key-pair (`.pem` file) | | Repository | variables | PROJECT_ADMINS | `["github_user_1", "github_user_2"]` | For example `["josix"]` | From fd20849c39dcc08fbe1f6b7ebd450fc4ff249cae Mon Sep 17 00:00:00 2001 From: iknowright Date: Tue, 11 Jul 2023 21:18:56 +0800 Subject: [PATCH 8/9] add google storage json to cd --- .github/workflows/cd.yml | 3 +++ document/continuous_deployment.md | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9d570f502..328438fdf 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -20,6 +20,9 @@ jobs: - name: Generate .env for staging vm from github secrets run: | echo "${{secrets.PRODUCTION_DOT_ENV_FILE}}" > .env + - name: Generate google-cloud-storage.json to src from secrets + run: | + echo "${{secrets.PRODUCTION_GOOGLE_CLOUD_STORAGE_JSON}}" > src/google-cloud-storage.json - name: Decode private key file for OpenSSH access over Ansible run: | echo "${{secrets.SSH_PRIVATE_KEY}}" | base64 --decode > "private.pem" diff --git a/document/continuous_deployment.md b/document/continuous_deployment.md index 175ab5724..e9fa15b25 100644 --- a/document/continuous_deployment.md +++ b/document/continuous_deployment.md @@ -28,6 +28,7 @@ So kindly configure project's action setting as the following: | Level | Type | Name | Value (example) | Remarks | |-----------|------------|---------------|----------|------------| | Repository | secrets | PRODUCTION_DOT_ENV_FILE | `DATABASE_URL=...` | multiline support | +| Repository | secrets | PRODUCTION_GOOGLE_CLOUD_STORAGE_JSON | `{ ...` | multiline support | | Repository | secrets | VM_USERNAME | cd_user | user name for ssh {user_name}@{vm_domain} | | Repository | secrets | VM_DOMAIN_IP | staging.pycon.tw | IP address or Domain that points to the staging server | | Repository | secrets | VM_PYTHON_INTERPRETER | `/home/dev/.pyenv/shims/python` | path to your python environment that has docker/docker-compose packages installed | From 2a30e2c7302642a5bd9af16d3e0f7e5ce89a8e14 Mon Sep 17 00:00:00 2001 From: iknowright Date: Mon, 2 Oct 2023 21:56:21 +0800 Subject: [PATCH 9/9] docs(cd): improve description for continuous deployment --- README.md | 3 ++- document/continuous_deployment.md | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f05554e40..ae3255339 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ We strongly recommend you configure your editor to match our coding styles. You ## Deployment +### Release to Production For site administrators, please refer to [document/deploy_docker_prod.md](/document/deploy_docker_prod.md). ### Continuous Deployment -Currently this is only for continuous deployment on staging server, please refer to [document/continuous_deployment.md](/document/continuous_deployment.md). +Currently, continuous deployment is only integrated on PyCon's staging server, please refer to [document/continuous_deployment.md](/document/continuous_deployment.md) for the setup. diff --git a/document/continuous_deployment.md b/document/continuous_deployment.md index e9fa15b25..ad835daba 100644 --- a/document/continuous_deployment.md +++ b/document/continuous_deployment.md @@ -2,6 +2,33 @@ The following describes how to setup continuous deployment for staging server. This setup presumes the site administrators have site deployment practices based on the docker production deployment [document/deploy_docker_prod.md](/document/deploy_docker_prod.md). +# Introduction of CI/CD, GitHub Actions, Ansible and related settings +Continuous integration (CI) +- Refers to the build and unit testing stages of the software release process. Every revision that is committed triggers an automated build and test. + +Continuous delivery / Continuous Deployment (CD) +- Usually as the next step for Continuous Integration, the code revision is built and tested in the application is automatically released to the production environment. + +GitHub Actions +- A CI/CD platform or service provided by GitHub. It provides public runners with limited compute minutes to run CI/CD workflows defined at `.github/workflows` directory. We can also provision custom Github Actions runner to perform CI/CD task. + +Ansible +- An automation tool that utilize *playbook* and *inventory* to manage production servers (nodes), such as sending commands, file transfers, system maintenance without manually setup via SSH/Remote session for it. + +Github Settings for secrets and variables +- CI/CD workflows for Github Actions often obtain sensitive information, credentials or variable. In project settings, Github provides secrets vault and variable holder to store these information in the secure manner and able to retrieve and use these values when the workflows run. + +## High level comparison of CI/CD in this project +| Conventional - Release | Github Actions + Ansible - Release | +|-----------|------------| +| `ENV` values managed by site admin | `ENV` values are set in project settings (secrets) | +| Site admin solely manage the production server | Site admin gives rights to github actions to deploy release to the production server | +| Site admin knows every deployment steps for docker | Deployment steps are defined in Ansible playbook (so everyone can understand deployment steps) | +| Site admin runs commands in a SSH session | Ansible runs the commands to server as defined by the playbook | +| Only admins have the server IP and private key | Server IP and private key are securely kept at github settings | +| Release is manual | Release automatically once code merges to `master` branch | +| Things are executed by hands | Things are executed by Github Actions' runner | + ## Requirements for Staging Server The staging server should have the following installed: - Docker 17.09+ (since we use `--chown` flag in the COPY directive) @@ -42,7 +69,7 @@ Reference - `base64 -i key.pem` (mac) - `cat key.pem | base64` (linux) -## Review +## CD Workflow Rules ### Events that triggers the pipeline 1. When the PR merges to `master` - no authorization needed, as PRs normally reviewed before merge