iT邦幫忙

2021 iThome 鐵人賽

DAY 10
1
DevOps

關於我幫新公司建立整套部屬流程那檔事系列 第 10

EP10 - Django 持續整合持續部署使用 Jenkins 和 AWS CodeDeploy

有 Jenkins、有 Gitlab、
有 Web Portal 又有給 Web Portal 部署的 EC2,
看來萬事俱備只欠東風,
而我們今天終於要把整串持續整合和持續部署串起來,
持續整合當然是用 Jenkins,
持續部署的部分我們先用 AWS CodeDeploy 建置,
透過撰寫 Jenkinsfile,
建立整條流水線,
從程式碼建置成品上 S3,
最後使用 AWS CodeDeploy 部署到 EC2。

前置步驟

本次我們要要使用 CodeDeploy 進行部署
所以我們理所當然要建立一個 CodeDeploy 的實例
使用 CodeDeploy 部署到 EC2
會使用到 EC2 上綁定的 IAM Role
沒有綁定 IAM Role 或是沒有相對應的權限
則沒有權限存取為 EC2 部署程式

在 Jenkins CI 的過程中
我們會將現在的版本製作一份放到 S3 上
所以需要多創建一個 Bucket 存放
將資料往 AWS S3 上丟會跑一個有 aws-cli 的 Docker
(不會在 Jenkins Server 上裝 aws-cli)
因此也需要多創建一個有「上傳到 S3」和「CodeDeploy CreateRelease」權限的 IAM

雖然不會在 Jenkins 上安裝 aws-cli
但是沒有另外創建 agent
Pipeline 中的每個步驟都會在 Jenkins Server 上跑
所以需要在 Jenkins Server 上安裝 Docker
這部分會在下面多做描述

S3 Bucket

前幾天我們建立了一個 S3 的 bucket 給 terraform 存放 tfstate 使用
為了方便分類
所以我們也另外開了一個 Bucket
給 CI/CD 的時候使用
可以在每次 push 的時候
就會 Gitlab 就會透過 webhook 自動 trigger Jenkins
在建置過程就將這次的修改打包一份上 S3 存放

resource "aws_s3_bucket" "artifactory" {
    bucket = "ithome-ironman-markmew-jenkins"
    acl    = "private"
    
    tags = {
        Name     = "Jenkins Artifactory"
        Creator  = "Terraform"
    }

    versioning {
        enabled = true
    }
}

AWS CodeDeploy

什麼是 AWS CodeDeploy

AWS CodeDeploy 是全受管部署服務
可自動將軟體部署到各種運算服務
包括 Amazon EC2、AWS Fargate、AWS Lambda 和現場部署伺服器

CodeDeply 在建立部署的時候
只支援 S3 和 Github 兩種來源
這也是我們剛剛要建立一個 Bucket 的原因
要將成品放到 S3 上
在 CD 時,再從 S3 抓取封存的檔案進行部署

為 EC2 建立 IAM Role 使用 Terraform

建立給 IAM 使用的 Role
在撰寫 Terraform 的時候
其實不只是 Role 而已
它還是 instance profile
需要多建立 aws_iam_instance_profile
才能跟 ec2 做綁定

權限的部分我們要綁定 CodeDeploy 和 S3 ReadOnly 的權限
沒有設定 S3 的權限
會造成部署的時候下載 Bundle 下載不下來

resource "aws_iam_instance_profile" "ec2_profile" {
    name = "ec2-profile"
    role = aws_iam_role.ec2_role.name
}

resource "aws_iam_role" "ec2_role" {
    name = "ec2-role"
    path = "/"

    assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "codedeploy.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "ec2_role_codedeploy_role" {
    policy_arn = "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole"
    role       = aws_iam_role.ec2_role.name
}

resource "aws_iam_role_policy_attachment" "ec2_role_s3_readonly" {
    policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
    role       = aws_iam_role.ec2_role.name
}

EC2 portal 連接 IAM Role

剛剛建立好的 iam role 要連接到昨天建立給 portal 使用的 EC2
在 main.tf 中增加 iam_instance_profile

main.tf

resource "aws_instance" "ithome_ironman_portla" {
    .
    .
    .
    hibernation             = false
    iam_instance_profile    = aws_iam_instance_profile.ec2_profile.name

    .
    .
    .
}

建立 CodeDeploy 使用 Terraform

建立 CodeDeploy 的程式碼就比較容易理解
需要先建立一個部署的 app
然後要建立部署的目標群組
這目標群組是用 tag 來做選擇
tag 在 aws 上主要用途也是如此
除了一般標記讓你容易辨別以外
有些服務會需要特別下特定的 tag
才能夠被辨識出來

resource "aws_codedeploy_app" "portal" {
    name = "ithome-ironman-portal"
}

resource "aws_codedeploy_deployment_group" "portal" {
    app_name              = aws_codedeploy_app.portal.name
    deployment_group_name = "ithome-ironman-portal"
    service_role_arn      = aws_iam_role.ec2_role.arn
    
    ec2_tag_set {
        ec2_tag_filter {
            key   = "Name"
            type  = "KEY_AND_VALUE"
            value = "ithome ironman 2021 portal"
        }
    }
    
    auto_rollback_configuration {
        enabled = true
        events  = ["DEPLOYMENT_FAILURE"]
    }
}

IAM for Jenkins

在 CI 的時候
Jenkins 會先將現在的 Code 打包一份送到S3
這部分不考慮在 Jenkins EC2 上先 config
純粹只是不想在 Jenkins 上裝太多東西而已

此外 Jenkins 相對重要
雖然 config 在 server 上和起 Docker 的同時把 config mount 上去差不多
但是這部分我期望多做一步繞個遠路
萬一 Jenkins 被打
也不會很快的所有資料被看光
也因為不是用 SSH 連到 EC2 Portal
只能使用 aws cli (api) 來呼叫
所以運作上反而相對單純獨立

main.tf

resource "aws_iam_user" "jenkins" {
    name = "Jenkins"
    path = "/"
    
    tags = {
        Name    = "Jenkins"
        Usage   = "Jenkins"
        Creator = "Terraform"
    }
}

resource "aws_iam_access_key" "jenkins" {
    user = aws_iam_user.jenkins.name
}

resource "aws_iam_user_policy" "jenkins_s3_upload" {
    name = "JenkinsS3Upload"
    user = aws_iam_user.jenkins.name
    
    policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::ithome-ironman-markmew-jenkins"
      ]
    }
  ]
}
EOF
}

resource "aws_iam_user_policy" "jenkins_create_deployment" {
    name = "JenkinsCreateDeployment"
    user = aws_iam_user.jenkins.name
    
    policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "codedeploy:CreateDeployment",
      "Effect": "Allow",
      "Resource": "arn:aws:codedeploy:ap-northeast-1:776212102166:deploymentgroup:ithome-ironman-portal/ithome-ironman-portal"
    },
    {
      "Action": [
        "codedeploy:GetDeploymentInstance",
        "codedeploy:GetDeploymentGroup",
        "codedeploy:ListDeploymentInstances",
        "codedeploy:GetDeploymentConfig",
        "codedeploy:ListTagsForResource",
        "codedeploy:GetDeployment",
        "codedeploy:ListDeployments"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:codedeploy:ap-northeast-1:776212102166:deploymentgroup:ithome-ironman-portal/ithome-ironman-portal",
        "arn:aws:codedeploy:*:776212102166:deploymentconfig:*"
      ]
    },
    {      
      "Action": "codedeploy:RegisterApplicationRevision",
      "Effect": "Allow",
      "Resource": "arn:aws:codedeploy:ap-northeast-1:776212102166:application:ithome-ironman-portal"
    }
  ]
}
EOF
}

outputs.tf

output "jenkins_access_key_id" {
    description = "The access key ID"
    value       = aws_iam_access_key.jenkins.id
}

output "jenkins_secret_token" {
    description = "Decrypt access secret key command"
    value       = aws_iam_access_key.jenkins.secret
    sensitive   = true
}
terraform apply

因為 secret token 是機密資訊
所以在 terraform apply 的時候會隱藏
需要再多輸入 output 指令才能輸出 secret token
在使用 terraform 建立存取金鑰的時候
這些資料就會紀錄在 tfstate 裏面
基於上次所提到的 pem key 以及這次 secret token
我相信更可以理解為什麼 tfstate 不應該進版控
這樣等同是把帳密資訊一併進版控

terraform output jenkins_secret_token

EC2 安裝 Agent

恩,對
你沒看錯
要在 EC2 上安裝 CodeDeploy Agent
雖然步驟很簡單
但是沒有裝 Agent 會部署不上去

安裝 ruby

sudo apt install ruby-full

下載 codedeplopy agent

cd /home/ubuntu
wget https://aws-codedeploy-ap-northeast-1.s3.ap-northeast-1.amazonaws.com/latest/install

安裝 codedeploy agent

chmod +x ./install
sudo ./install auto > /tmp/logfile
sudo ./install auto -v releases/codedeploy-agent-###.deb > /tmp/logfile

持續整合與持續部署

AWS 上持續部署所需要的基礎設施先告一段落
但是我們還沒在 Jenkins 上建立 Pipeline
之前有提到我們不打算在 Jenkins 的 EC2 上安裝 aws-cli
取而代之的則是起 Docker 來執行 aws-cli
剛剛為 Jenkins 建立的 IAM User
在 Pipeline 中除了會 mount IAM User 的設定
將建置結果上傳到 S3 以外
也會呼叫 CodeDeploy 建立一個新的版本
透過 CodeDeploy 將服務部署到 EC2

Jenkins Server 安裝 Docker

Adding Docker’s GPG Key

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

Installing the Docker Repository

sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu  $(lsb_release -cs)  stable"

Installing the Latest Docker

sudo apt update
sudo apt-get install docker-ce

Verifying Docker Installation

docker --version

https://ithelp.ithome.com.tw/upload/images/20210922/201415186g6KJtwNBx.png

Start and Enable Docker

sudo systemctl start docker
sudo systemctl enable docker

更改 docker.sock 權限與使用方式

sudo usermod -aG docker jenkins
sudo usermod -aG root jenkins
sudo usermod -aG ubuntu jenkins
sudo chmod 644 /var/run/docker.sock
sudo chown jenkins:docker /var/run/docker.sock 

Jenkins 安裝套件

到「管理 Jenkins」中的「管理外掛程式」
選擇「Docker Commons」和「Docker Pipeline」進行安裝

https://ithelp.ithome.com.tw/upload/images/20210922/20141518aCLP7hwjbC.png

IAM User config in EC2 server

早期大家在做 CI/CD 時
都會在 VM 上藏 SSH Key
若是沒有控管好權限
Jenkins 又沒有定期修補漏洞
在 Jenkins 被攻破後就有可能全部淪陷
其實搬上雲端也是一樣的道理
盡可能給予適當的權限即可

雖然我們在 Jenkins Server 上藏了 IAM User 的 Key 和 Token
但是權限上我們只限制上傳到 S3
以及 Create Release 的權限而已

建立資料夾

sudo mkdir /usr/local/src/aws_docker_file
sudo mkdir /usr/local/src/aws_docker_file/.aws

建立檔案

sudo touch /usr/local/src/aws_docker_file/.aws/config
sudo touch /usr/local/src/aws_docker_file/.aws/credentials

config aws cli information

不是使用 aws config 去設定
是實際創建檔案
為了起 Docker 時才能夠把 IAM user mount 上去

/usr/local/src/aws_docker_file/.aws/config

[default]
region = ap-northeast-1
output = json

將 terraform 做出來的 key id 和 secret 填入
/usr/local/src/aws_docker_file/.aws/credentials

[default]
aws_access_key_id = 你的 KEY
aws_secret_access_key = 你的 Secret

Jenkinsfile

credentialsId 前幾天有在 Jenkins 裡面添加
這個直接拿來用就可以了

withDockerContainer 顧名思義是起一個 container
並在內部執行相關指令
因為我們只是個初始化專案
所有跑測試一定會通過
「Run Test」這部分是為了以後可能有撰寫測試而留的

「Archieve Project」是使用 git 的指令將程式碼另外壓縮
會另外下這個指令
也是因為 .git 裡面包含太多資訊
如果不是另外 export 沒有 .git 的乾淨版本
會更容易被試出系統的漏洞

pipeline {
  agent any
  
  stages {
    stage('Git Checkout') {
      steps {
        sh 'pwd'
        sh 'ls -a'
        retry(3) {
          dir('ithome-ironman') {
            git branch: 'develop',
            credentialsId: '你的Credentails',
            url: 'git@你的IP或HOST:ithome-ironman-2021/portal.git'
          }
        }
      }
    }
    stage('Run Test') {
        steps {
            echo 'Run Python Unittest ...'
            dir('ithome-ironman') {
                script {
                    withDockerContainer(image: 'python:3.7.10-buster', args: '-u root:root') {
                        sh """
                        apt-get install libpq-dev
                        pip install --user -r requirements.txt
                        python manage.py test
                        rm -rf __pycache__
                        rm -rf */__pycache__
                        rm -rf */*.pyc
                        """
                    }
                }
            }
        }
    }
    stage('Archieve Project') {
        steps {
            echo 'Archieve...'
            dir('ithome-ironman') {
                sh 'git archive --format=tar.gz --output ./portal.tar.gz HEAD'
            }
        }
    }
    stage('Upload to S3') {
        steps {
            echo 'Upload...'
            dir('ithome-ironman') {
                sh "docker run --rm -v ${WORKSPACE}/ithome-ironman:/app -v /usr/local/src/aws_docker_file/.aws:/root/.aws mikesir87/aws-cli aws s3 cp /app/portal.tar.gz s3://ithome-ironman-markmew-jenkins/portal/stage/portal-${env.BUILD_ID}.tar.gz"
            }
        }
    }
    stage('Deploy') {
        steps {
            echo 'Deploy ...'
            dir('ithome-ironman') {
                sh "docker run --rm -v /usr/local/src/aws_docker_file/.aws:/root/.aws mikesir87/aws-cli aws deploy create-deployment --application-name ithome-ironman-portal --deployment-config-name CodeDeployDefault.OneAtATime --deployment-group-name ithome-ironman-portal --s3-location bucket=ithome-ironman-markmew-jenkins, bundleType=tgz, key=portal/stage/portal-${env.BUILD_ID}.tar.gz"
            }
        }
    }
  }
}

程式碼添加部署指令

裝完 Agent
還是要在專案底下
新增一些設定檔
appspec.yaml 在撰寫時要注意
如果檔案不存在會部署失敗
之前手動部署上去的檔案要先刪掉,不然也會部署失敗
至於 version 好像也只能是 0.0
有試著改成 0.1 或是 1.0 也都會失敗

scripts/start_server

source /var/www/venv/portal/bin/activate/bin/activate
pip install -r /var/www/portal/requirements.txt
service apache2 start

scripts/stop_server

service apache2 stop

scripts/install_dependencies

#!/bin/bash
apt-get update
apt-get install pip3
apt-get install python3 python3-virtualenv python3-pip libpq-dev python-dev
cd /var/www/venv
virtualenv portal
source portal/bin/activate
pip install -r /var/www/portal/requirements.txt

appspec.yml

version: 0.0
os: linux
files:
 - source: /manage.py
   destination: /var/www/portal
 - source: /requirements.txt
   destination: /var/www/portal
 - source: /portal/
   destination: /var/www/portal/portal

permissions:
  - object: /var/www/portal/manage.py
    owner: ubuntu
    mode: 644
    type:
      - file
hooks:
  AfterInstall:
    - location: scripts/install_dependencies
      timeout: 300
      runas: root
    - location: scripts/start_server
      timeout: 300
      runas: root

  ApplicationStop:
    - location: scripts/stop_server
      timeout: 300
      runas: root

今天的資訊量有點多又有點雜
AWS CodeDeploy 只允許 Github 和 S3 兩個來源
所以我們需要先創建一個 Bucket
將 CI 過程中的 Code 打包一份上 S3

為了建立 AWS CodeDeploy
需要幫 EC2 建立 iam profile 並綁定 AWS CodeDeploy 和 S3 讀取權限
即使如此還是需要在 EC2 上裝 CodeDeploy Agent
這樣才能在 Pipeline 的最後 Create Deployment
CodeDeploy 進入 EC2 進行部署的時候才能順利去 S3 抓資料

在 Jenkins 執行 CI/CD 的過程
需要用到 Docker
所以需要在 Jenkins 的 EC2 裝 Docker
以及在 Jenkins 上裝一些套件

我其實不太想要說什麼了
大家照著步驟做當然可以部署在 on-premise 的機械上
但是專案內新增部署流程不說
IAM 的權限設定綁這麼死
網路 inbound/outbound 綁這麼死
CodeDeploy 還要在 on-premise 裝 Agent

難怪大家都不太愛寫 AWS Cloud 的教學文
除了貴又麻煩以外
還要把 AWS 的每份文件都翻過好幾輪
才知道原來有些文章和細節真的要仔細看
不然做不出來真的會想要 Grant Administrator 權限給它就好了

參考資料:

  1. AWS CodeDeploy
  2. Step 3: Create a service role for CodeDeploy
  3. Terraform Resource: aws_iam_user
  4. How To Install and Use Docker on Ubuntu 20.04
  5. Docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
  6. Install the CodeDeploy agent for Ubuntu Server

上一篇
EP09 - 建立 Django 專案和 EC2 環境 並手動部署到 EC2
下一篇
EP11 - 為你的 portal 添加 Load Balance 和掛載 Web ACLs
系列文
關於我幫新公司建立整套部屬流程那檔事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言