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

<<:  如何衡量万事万物 (1) 衡量的定义

>>:  [Day-22] R语言 - 分群应用(三) 相异点侦测 ( detect dissimilar point by clustering in R.Studio )

Day_11 : 让 Vite 来开启你的 Vue 之 Config 常见配置 (Vite 最终篇 XD)

Hi Dai Gei Ho~ 我是Winnie~ 延续上篇没有说完的内容,今天我们要来看看 Vite...

json档删除符合条件的特定事件该怎麽做?

大家好,我是用python程序 在输出json档之後想做两件事但没有头绪,希望有人可以帮我解惑。 以...

Day21-D3 基础图表:散点图/散布图

本篇大纲:基本散布图范例、进阶散布图范例 今天的一天一图表,我们要来画 散点图 / 散布图!散布图...

Day 08 Create a classification model with Azure Machine Learning designer

Classification - Predict category or class Train r...

Day14 v-cloak与v-pre

今天再多来看看两个Vue的指令,v-cloak与v-pre v-cloak 使用v-cloak的原因...