使用 CDK 建置 AWS CodeDeploy 應用於 AutoScaling (一)

POSTED BY   Chris
2020 年 12 月 6 日
AWS, CDK
使用 CDK 建置 AWS CodeDeploy 應用於 AutoScaling (一)

CI/CD 近年來已經從火紅的新技術,到現在快變成必備技能,雖然工具繁多,但會個一、二套,了解原理後,使用其他套也比較能得心應手,再加上 IaC (Infrastructure as Code) 大行其道,也有了愈來愈方便的工具,如 AWS CDK,讓整個從 Infra 建置到完成 CI/CD,都可以用程式碼來自動完成

這篇文章針對 AWS CodeDeploy 應用在 AWS Auto Scaling 來做詳細說明,希望對來觀看的讀者有幫助,而為什麼會選擇使用 AWS Auto Scaling 來應用,因為就我個人而言 CodeDeploy 一台機器沒感覺,但 CodeDeploy 6 台或 10 台以上,可能就會很有感,自動化的好處就在量多的時候能得到相當的好處

 

概述

AWS CodeDeploy 主要分兩個部分,第一個部分是建置 AWS CodeDeploy Infra,第二個部分是 Application 發佈新版本時,觸發 AWS CodeDeploy 流程,此次實作 CDK 主要負責 AWS CodeDeploy Infra,而 Travis CI 使用來觸發 AWS CodeDeploy 流程

 

開發前準備

對於 CDK 如果比較不熟的話,建議可以先看 10 分鐘快速了解 AWS CDK 相關名詞

 

Travis CI 觸發 CodeDeploy 流程圖

AWS CodeDeploy Flow
Hint: 點圖片可放大

流程圖是以此次實作的現況來做說明,也可以參考 AWS 官方的就地部署流程圖說明,會更加清楚 AWS CodeDeploy 的運作方式,方便後續實作上的理解

會先解說 Application CodeDeploy 流程最主要這是我們 CD 的目的,也就是每次 Github Push 新版本時,AWS CodeDeploy 能幫我們自動化 CD 的流程,而要達到這個目的之前,當然是需要先建置 Infra 的部分

 

第一部分:建置 AWS CodeDeploy Infra

主項目

  • S3 Bucket
  • IAM role、 IAM user
  • CodeDeploy Application
  • CodeDeploy DeployGroup
  • Auto Scaling Group
    • VPC
    • AWS Systems Manager – AWS-ConfigureAWSPackage (自動在每台 EC2 上安裝 CodeDeploy agent)
  • Auto Scaling LaunchConfiguration
以上主項目也就是基本配備,要達成 AWS CodeDeploy 在 AWS Auto Scaling 上運作的最基本項目

其他項目

  • AWS Systems Manager – AWS-RunShellScript (此實作使用 php lumen framework 來當 Application,故需要安裝 php)
  • CfnWaitCondition (由於需要安裝 php,等於此實作用了兩個 SSM 去安裝 CodeDeploy Agent 和 php,導致 ubuntu apt 在運作時會因為有另一個 process 在運作導致出錯,故必需等其中一方安裝完畢後,才能再跑下去,避免這個問題)
其他項目是因為這次實作遇到的問題必需要解決的,在別的情境和環境中不一定會遇到

對於使用 CfnWaitCondition 再特別說明一下,剛使用 CDK 時可能會覺得不是有 addDependsOn 方法可以用,可以決定 Resources 的執行順序,還會有這個問題嗎?但實際上是,addDependsOn 是可以決定執行順序沒錯,但每個 Resource 實際上執行的時間不一樣,並沒辦法控制等 A Resource 完全執行完畢後,再執行 B Resource,所以才會有這個問題

 

S3 Bucket

用來放新版本的程式碼,如下程式碼

TypeScript
const bucket = new s3.Bucket(this, 'CodeDeploymentBucket');

這行屬於 CDK L2 Construct 的 Resource,一行即可簡單完成,會先建置 S3 Bucket 是因為後續建立 IAM 時,會需要用到 bucket name

 

IAM role、 IAM user

(1) CodeDeployServiceRole

最主要賦予 CodeDeploy Service 權限,如下程式碼

TypeScript
protected createCodeDeployServiceRole(): iam.Role {
    return new iam.Role(this, 'CodeDeployServiceRole', {
      roleName: 'CodeDeployServiceRole',
      assumedBy: new iam.ServicePrincipal(
        `codedeploy.${this._region}.amazonaws.com`
      ),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'service-role/AWSCodeDeployRole'
        ),
      ],
      inlinePolicies: {
        'auto-scaling-group-with-launch-template': new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: ['ec2:RunInstances', 'ec2:CreateTags', 'iam:PassRole'],
              resources: ['*'],
            }),
          ],
        }),
      },
    });
  }
  • assumedBy: 由誰來擔任此角色,這裡 codedeploy.${this._region}.amazonaws.com 指的就是 CodeDeploy 服務本身
  • managedPolicies: 從 AWS 既定的 IAM Policy 中直接指定,要特別注意 service-role/ prefix 不是每個既定的 IAM Policy 都有這個 prefix

 

(2) CodeDeployEC2Role

最主要賦予 Auto Scaling 中 EC2 的權限,如下程式碼

TypeScript
 protected createCodeDeployEC2Role(): iam.Role {
    return new iam.Role(this, 'CodeDeploy-EC2', {
      roleName: 'CodeDeploy-EC2',
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'AmazonSSMManagedInstanceCore'
        ),
      ],
      inlinePolicies: {
        'CodeDeploy-EC2-Permissions': new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: ['s3:Get*', 's3:List*'],
              resources: [
                `arn:aws:s3:::aws-codedeploy-${this._region}/*`,
                `arn:aws:s3:::${this._codeDeployBucketName}/*`,
              ],
            }),
          ],
        }),
      },
    });
  }
  • assumedBy: 由誰來擔任此角色,這裡 ec2.amazonaws.com 指的就是 EC2 本身
  • managedPolicies: 由於每台 EC2 的 CodeDeploy Agent 是由 SSM 自動安裝的,故需要給予 SSM 權限
  • inlinePolicies: 這邊的自訂權限最主要讓 EC2 能夠在到 S3 拉取新版本下來部署,也就是 CodeDeploy 流程圖中的第 5 個步驟

 

(3) CodeDeployEC2InstanceProfile

EC2 需要透過 instance profiles 來綁定 IAM Role,如下程式碼

TypeScript
protected createCodeDeployEC2InstanceProfile(
    assignedRole: iam.Role
  ): iam.CfnInstanceProfile {
    return new iam.CfnInstanceProfile(this, 'CodeDeploy-EC2-Instance-Profile', {
      roles: [assignedRole.roleName],
      instanceProfileName: 'CodeDeploy-EC2-Instance-Profile',
    });
  }
  • roles: 這邊就是指定在 (2)CodeDeployEC2Role 所創建立出來的角色
  • instanceProfileName: 取一個方式識別的名字即可
在 AWS Console Management 操作時, (2) 和 (3) 是共同創建無法分開的,但用 AWS CLI 或 AWS CDK 是可以分開設定

 

(4) TravisCICodeDeployAppUser

最主要賦予 Travis CI 可以執行的權限,如下程式碼

TypeScript
protected createTravisCICodeDeployAppUser(): iam.User {
    const user = new iam.User(this, 'TravisCIUser-Codedeploy-App', {
      userName: 'TravisCiUser-Codedeploy-App',
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'AWSCodeDeployDeployerAccess'
        ),
      ],
    });

    user.attachInlinePolicy(
      new iam.Policy(this, 'PushS3BucketForCodeDeploy', {
        policyName: 'PushS3BucketForCodeDeploy',
        statements: [
          new iam.PolicyStatement({
            actions: ['s3:PutObject'],
            resources: [`arn:aws:s3:::${this._codeDeployBucketName}/*`],
          }),
        ],
      })
    );

    return user;
  }
  • managedPolicies: AWSCodeDeployDeployerAccess 權限最主要用來可以 Create Deployment,也就是 Travis CI 要能通知 CodeDeploy 有新的部署事件產生

  • user.attachInlinePolicy 這個部分最主要讓 Travis CI 能夠 push 新版本到 S3 去,故需要 s3:PutObject

這邊建立的是 IAM User,不同於 IAM Role,IAM User 可以產生 AWS Credential 來使用,可設定於 Travis CI 後台

 

Auto Scaling Group

處理完 IAM 後,再來就是 Auto Scaling Group,會先處理 Auto Scaling Group 是因為後續的 CodeDeploy Infra 中會需要指定 Auto Scaling Group 相關的參數,依順序來說,這樣處理會比較方便,而另一個選擇是在 CDK 撰寫中,也可以使用 Lazy values 來處理順序問題

Auto Scaling Group 主題也不少,這邊僅就此次實作用到的東西來介紹

(1) AWS Systems Manager – AWS-ConfigureAWSPackage

用來在每台 EC2 上安裝 CodeDeploy Agent,如下程式碼

TypeScript
protected createCfnSSMInstallCodeDeployAgent(): ssm.CfnAssociation {
    return new ssm.CfnAssociation(this, 'ASG-SSM-CodeDeployAgent', {
      name: 'AWS-ConfigureAWSPackage',
      targets: [{ key: 'tag:Name', values: ['CodeDeployDemo'] }],
      parameters: { action: ['Install'], name: ['AWSCodeDeployAgent'] },
      scheduleExpression: 'cron(0 18 ? * SUN *)',
    });
  }
  • name: 這邊的 name 就是 SSM 要執行指令的種類,可參考 AWS Systems Manager documents
  • targets: 也就是要安裝在哪台 EC2,由於我們要在 Auto Scaling Group 中的每台 EC2 都執行,所以不是指定 EC2 instance id,而且用 tag name 的方式來指定
  • parameters: 選用了 AWS-ConfigureAWSPackage Document 後,就是指定動作和名稱,也就是安裝 AWSCodeDeployAgent

 

(2) EC2 Auto Scaling Launch Configuration

在建立 Auto Scaling Group 前, 需要先建立 Launch Configuration,用於定義在 Auto Scaling 中的 EC2 的規格,如下程式碼

TypeScript
protected createCfnLaunchConfiguration(
    iamInstanceProfile: string | undefined
  ): autoscaling.CfnLaunchConfiguration {
    return new autoscaling.CfnLaunchConfiguration(this, 'ASG-config', {
      launchConfigurationName: 'CodeDeployDemo-AS-Configuration',
      imageId: 'ami-02b658ac34935766f',
      instanceType: 't3.micro',
      iamInstanceProfile: iamInstanceProfile,
    });
  }
  • imageId: 也就是 EC2 的 AMI id,這次實作是選擇 ubuntu 18.04 的 AMI
  • instanceType: 練習實作用便宜一點的機器,所以選 t3.micro
  • iamInstanceProfile: 這邊要特別注意,這邊的寫法是由參數帶過來,要填入之前 IAM role、 IAM user(3) CodeDeployEC2InstanceProfile 所建立的 EC2InstanceProfile,這樣每台 EC2 才會有對的權限可以執行相關動作

 

(3) VPC

在建立 Auto Scaling Group 前,需要先建立 VPC,如下程式碼

TypeScript
protected createCodedeployVpc(): ec2.Vpc {
    return new ec2.Vpc(this, 'codedeploy-vpc', {
      cidr: '10.0.0.0/16',
      natGateways: 0,
      maxAzs: 4,
    });
  }
  • cidr: 建立最基本的網段 10.0.0.0/16,詳細可參考 VPC_Subnets
  • natGateways: 一般是讓 vpc 內不對外服務的 Resource,可以做一般套件更新用的通道,目前練習用,所以不用開,特別注意 natGateways 一開下去就會立刻算費用,而且不包含在 free tier 中
  • maxAzs: 指定 VPC 可以在哪些可用區上運行,由於此次實作用的是 tokyo region (ap-northeast-1),available zone 有 4 個,所以指定為 4 ,這個部分可以改成動態由 region 抓取最大的 az 值 (cdk.Fn.getAzs),也可以由設定檔指定,待修改

 

(4) Auto Scaling Group

建立 Auto Scaling Group,如下程式碼

TypeScript
  protected createCfnAsg(
    launchConfigurationName: string | undefined,
    vpc: ec2.Vpc
  ): autoscaling.CfnAutoScalingGroup {
    const cfnAsg = new autoscaling.CfnAutoScalingGroup(this, 'ASG', {
      autoScalingGroupName: 'ASG-with-codedeploy',
      minSize: '6',
      maxSize: '8',
      launchConfigurationName,
      availabilityZones: cdk.Fn.getAzs(this._region),
      vpcZoneIdentifier: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PUBLIC,
      }).subnetIds,
      tags: [{ key: 'Name', value: 'CodeDeployDemo', propagateAtLaunch: true }],
    });

    cfnAsg.cfnOptions.creationPolicy = {
      resourceSignal: {
        count: 6,
        timeout: 'PT5M',
      },
    };

    return cfnAsg;
  }
  • minSize: 最少開幾台 EC2
  • maxSize: 最多開幾台 EC2
  • launchConfigurationName: 就是剛才建立的的 Launch Configuration,Typescript 中如果 key 和 value 變數是同一個的話,可以省略 key 值
  • availabilityZones: 指定 Auto Scaling 可以開在哪些可用區上,這裡使用 CloudFormation 的 Fn::GetAZs 來取得可用區數量
  • vpcZoneIdentifier: 指定 subnets,subnets 由 vpc 而來,所以由剛才建立的 vpc 來設定,注意的事 subnetType 要設定正確,如果是對外服務,記得設定成 public
  • tags: 這邊也要特別注意,tag name 必需設定成和 SSM 指定相同的值,每台 EC2 才能順利安裝 CodeDeploy Agent

cfnAsg.cfnOptions.creationPolicy 先不做說明,這跟 WaitCondition 有關,待之後再說明

 

CodeDeploy Application、CodeDeploy DeployGroup

處理完 Auto Scaling Group 後,再來就是 CodeDeploy 本身的 Infra (內心的聲音:終於…)

(1) Application

建立 CodeDeploy Application,如下程式碼

TypeScript
protected createApplication(): codedeploy.ServerApplication {
    return new codedeploy.ServerApplication(this, 'CodeDeployApplication', {
      applicationName: 'LumenApp',
    });
  }
  • applicationName: 取一個方便識別名稱,如此次實作用 lumen 來建置,故取名 LumenApp

(2) DeploymentGroup

建立 CodeDeploy DeploymentGroup,如下程式碼

TypeScript
protected createDeploymentGroup(
    applicationName: string,
    serviceRoleArn: string,
    autoScalingGroupName: string
  ): codedeploy.CfnDeploymentGroup {
    return new codedeploy.CfnDeploymentGroup(
      this,
      'CodeDeployDeploymentGroup',
      {
        applicationName,
        serviceRoleArn,
        deploymentConfigName:
          codedeploy.ServerDeploymentConfig.ONE_AT_A_TIME.deploymentConfigName,
        deploymentGroupName: 'Master',
        autoScalingGroups: [autoScalingGroupName],
    );
  }
  • applicationName: 也就是剛才建立的 CodeDeploy Application
  • serviceRoleArn: 要填入之前 IAM role、 IAM user(1) CodeDeployServiceRole,讓 CodeDeploy DeploymentGroup 有權限可以執行

CodeDeploy Application 可以包含多個 DeploymentGroup,例如應用程式要部署在現行運作的機房上,也可能需要部署在備援的機器上,就實際狀況來做區分即可

 

部署 Infra

一切準備就敘後,可以開始部署,基本上跑 4 個指令就能完成

  • tsc
  • cdk bootstrap -c region=ap-northeast-1
  • cdk diff -c region=ap-northeast-1
  • cdk deploy -c region=ap-northeast-1

Deploy 後,可到 AWS Console Management 的 CloudFormation 中去查看 Deploy 狀態

另外如果要額外跑 lint 工具和 unit test,當然更好,若在 Travis CI 上設定可以參考這裡,也就是這次實作完整的設定

 

第二部分:建置 AWS CodeDeploy Application

CodeDeploy Infra 建置完後,再來就是應用程式上的設定,在每次要上新版本時,自動跑 CI 來做自動跑 CD,這樣就是一個比較完整的自動化過程

 

.travis.yml 設定

如下 yaml 檔

YAML
before_deploy:
  - composer install --optimize-autoloader --no-dev
  - mkdir -p .CodeDeploy
  - zip -rq .CodeDeploy/latest.zip app bootstrap database public resources routes storage vendor appspec.yml scripts .env

deploy:
  - provider: s3
    access_key_id: &aws_access_id_key
      secure: "KUgC3avzoOxEai2+0YLWKXKZ6aV3/XAUxRMj/DQspYfLAajj0TxPMt2jIQufMUZgGa00gTuVEXYs9nAOeeaJEQ7JFo7NC/cCHosUR2NSjpYDUHsrKEL2EaIFGK7xmKzHb7Yhx4KwqDPBhFXsKXU1tMfG/jsLqiQlgEQjXGdJsvlgyeYAwzizGvpMvgh+c/pWLOhHUv/zEZB/woxdDs7HMsG7X+J02cJm0l30z/BEKctNrBiCjKtIwDeCBPAYPQf/2wrTBPSMlmQd1sn5e2kOy0WTYCw/yaUGJwhuaqFnCHZhmPpq5l4i2pXTZrcj5M8cWGevmp1SAOe/CQe48+F+pwt+RBLV5Ivaafa8gERxpKQNnJ4lelOHnzj60IPVl9GB1hCoVCFdFaOS2/M4EASEMYLnJaZmeXCSrcuN0Rp0VumWMff+Ld3jk0MxOBHRCT8jxaLNchPfJ70wa4udm92UqMqr3OubDmVHkkBXqOqQU41XGW0fCDACPCbQEfUPEyK9ZrH5qHGBYPuBjgTzy/fehxqzYe517NwLQvZvXuvWEexp/33qtpmNEcOV0Skhb66C6H1kt+wDzQHfQNGU6Pk7Yp5vNlJVHhL5OOmq85HuyTsQUjWf6UDbivzLtXNs8/kPguZQobsn39cFDI0+VoeKB34Buw1RnslwAMqX4gYg0dU="
    secret_access_key: &aws_secret_key
      secure: "j7Na+dltq3BlYEpmZIpkgm8Gf2Kk0tTFl28heAsKgd0PwTJ3HZIjZKNU7EAFKkYBwOFuhRpKmVPPIyz3MTog9CtT1zzF1t5tp531MWfBxQVTMXPOSTO8ND8g0FscKHnqbF6BKEdzRRZGhnDa+MgdsNWdSoIVVMFsCQWTjPUIaTDQSWABm+hvfCO1I85M/e9ibqlNm4JWm1786ywEW9F11Pb+1KMZeokShwv7h1Som1W53HXqCR3u2mXUlwbvehRex0EcDi89sa9YTfCYZ8uAVPTqyIDhBstns5PLtZ1F9gEWKqeBbt4nl9f+1VTcOoOvkkA0aQrJRRr5alJXaM+nxC2YAZAKhLseo+Zz0Qcv/fRU3/WFwJxbGQu2STBwwamA9xvWLa87p2BYlNhLVTUmBSaN6oO7V3Q3Qo8c3eoLnor+dsjt7EXZ3PLXxhcN1aYLuwflfK1oiX6EDoVK2rapkf0RVM6LVmjXDQEt/TVdUZvwa2WPQa3hG1o4HMfT1IrbGMs2+LLksZ/T7C+UH3NECa64Wb5NA5sWniW8Ta1qLrTy27bpJWEfLKOJb9qeUyFCOTxSawgnh1dMePuoPssSRJld7RqGorQs+QjLLxiIkRowL6JQZCmBVSSmJtnoLV4yIKX5X2mDzR4t88+CoxtuI1GA3/+rlD01vR6clhRzvaI="
    bucket: &deployment_bucket awscodedeploywithautosca-codedeploymentbucket072a-1otat3gez9j4j
    local_dir: .CodeDeploy
    skip_cleanup: true
    region: ${DEPLOY_REGION}
    on: &deploy_branch
      branch: master
  - provider: codedeploy
    access_key_id: *aws_access_id_key
    secret_access_key: *aws_secret_key
    bucket: *deployment_bucket
    key: latest.zip
    application: LumenApp
    deployment_group: Master
    region: ${DEPLOY_REGION}
    on: *deploy_branch

完整的 .travis.yml 在此

  • before_deploy: Travis CI 生命週期在部署之前會做的事情,這邊很純單就是用指令把 code 打包成 zip 檔,這邊要特別注意,在正式 Production 環境中,不要把 .env 也打包進去,這邊為了實作方式才這樣做,具體上比較好的做法,可以使用 aws s3 file encryption 來保護 .env 環境設定檔中的資料

  • deploy: 正式部署所要做的事情

    • 第一組 provider 如下
      • provider: s3 : 把新版本程式碼打包上傳到 S3 (等於 Travis 幫忙做 aws deploy push
      • access_key_id 和 secret_access_key: 這邊使用第一部分 IAM role、 IAM user(4) TravisCICodeDeployAppUser 所建立出來的 IAM User 後,到 AWS Console Management 去建立一組 Credential,就是這兩個設定值,但要特別注意,這邊必需要用 travis encrypt 去加密這兩組字串,否則曝露在 Github 上可不是好玩的 的地方
      • local_dir: 就是在 before_deploy 中打包的 zip 檔所在位置
      • skip_cleanup: 設定為 true,避免 Travis CI 把 build 過程中的檔案刪除
      • on.branch: 在哪個 github branch 中做部署
    • 第二組 provider 如下
      • provider: codedeploy : 建立新的 Deployment (等於 Travis 幫忙做 create-deployment
      • bucket: s3 bucket name,也就是第一部分一開始建立 S3 bucket
      • key: before_deploy 時所壓縮出來的檔名
      • application: 在第一部分 CodeDeploy Application 建立時的 CodeDeploy Application name
      • deployment_group: 在第一部分 CodeDeploy DeployGroup建立時的 deploymentGroupName
      • 其餘屬性與第一組 provider 相同則不再贅述

 

appspec.yml 設定

在新版本部署時,每一台 EC2 會根據 appsec.yml 中設定的 hook ,來跑相對應的 script 做部署的動作,詳細可參考 AppSpec ‘hooks’ section for an EC2/On-Premises deployment

這次實作中用了四個 hook

  • BeforeInstall
  • AfterInstall
  • ApplicationStart
  • ValidateService

完整的設定在這裡

這邊也要注意 shell script 中撰寫的語法,實作中也有卡在 script 中判斷不正確,或語法錯誤,都會導致 deploy 失敗,不過失敗就修正語法就好,有時候只是需要耐心去完成而已

最後只要在指定的 branch push commit 去 Trigger Travis CI 後,如果 Travis CI build pass 後,就可以去 AWS Console Management 中的 CodeDeploy -> Deployments 去查看部署狀況,觀看其中 status 的狀態能知道是否成功,若失敗,再點進去看每個 hook 執行的狀況,就能除錯

 


 

以上的說明,完整的原始碼在以下的 Github Repo 中,README 中也有設定說明步驟可以參考

 

以上是大致上的過程,至於過程中難免一定會踩到一些雷,就待下一篇來說明,把踩雷當做自己變強的養份,這樣想在實作的過程中就比較不會那麼煩燥了 ^_^

歡迎留言
0

您可能也想看

Workaround for AWS Grafana alerting
2023 年 8 月 3 日
AWS, DevOps
AWS VPC Endpoint 使用場景
2022 年 3 月 14 日
AWS, CDK, Network
CDK 指定 Physical names 運作方式
2021 年 12 月 4 日
AWS, CDK, Cloudformation