[GitLab CD] 継続的デプロイ: Pipeline

デプロイを自動化してリリースの安定化が求められる
Pipelineにてjobを組み合わせ、デプロイをコードによって自動化する機能
Review Appsでアーティファクトの確認

### ステージの定義
.gitlab-ci.yml

job01:
  stages:
    - build
    - test
    - deploy

  Build_Job01:
    stage: build
    script:
      - "sudo docker run centos:latest echo Build01"

  Test_Job1:
    stage: test
    script:
      - "sudo docker run centos:latest echo Test01"

  Test_Job2:
    stage: test
    script:
      - "sudo docker run centos:latest echo Test02"

  Deploy_Job1:
    stage: deploy
    script:
      - "sudo docker run centos:latest echo Deploy01"

stageのリストに記載した順番に実行される
whenパラメータ: on_success, on_failure, always, manual

### whenパラメータの例

job01:
  stages:
    - build
    - cleanup_build

  Build_Job01:
    stage: build
    script:
      - "docker run centos:latest echo BuildProcess"

  Cleanup_Build_Job01:
    stage: cleanup_build
    script:
      - "docker rm -f $(sudo docker ps -qa --filter 'status=exited')"
    when: on_failure

### dependenciesパラメータの例

job01:
  stages:
    - build
    - test
    - deploy

  variables:
    APP_NAME: 'web_demo'
    CONTAINER_NAME: ${CI_PROJECT_NAME}_${APP_NAME}

  Build_app01:
    stage: build
    script:
      - cd ./app01
      - gradle war
    artifacts:
      paths:
        - app01/build/libs/*.war

  Build_app02:
    stage: build
    script:
      - cd ./app02
      - gradle war
    artifacts:
      paths:
        - app02/build/libs/*.war

  test_app01:
    stage: test
    script:
      - cd ./app01
      - gradle test
    dependencies:
      - build_app01

  test_app02:
    stage: test
    script:
      - cd ./app02
      - gradle test
    dependencies:
      - build_app02

  deploy:
    stage: deploy
    script:
      - cp app*/build/libs/*.war /usr/local/tomcat/webapps
      - systemctl start tomcat.service

  build_web_demo:
    stage: build
    image: gradle:4.4.1-jdk8
    script:
      - cd ./${APP_NAME}
      - gradle war
      - gradle test
    artifacts:
      paths:
        - ${APP_NAME}/build/libs/*war
      expire_in: 10 min
    tags:
      - docker
      - gitlab-runner01

  test_web_demo:
    stage: test
    variables:
      TEST_CONTENT: GitLab
    script:
      - docker run --name ${CONTAINER_NAME} -d ${APP_NAME}
      - sleep 5
      - export CONTAINER_ADDRESS='docker inspect -f "{{ .NetworkSettings.IPAddress}}" ${CONTAINER_NAME}'
      - wget --post-data="name=${TEST_CONTENT}" http://${CONTAINER_ADDRESS}:8080/${APP_NAME}/hello
      - grep ${TEST_CONTENT} ./hello
    after_script:
      - rm -v ./hello
      - docker rm -f ${CONTAINER_NAME} || true
    tags:
      - shell
      - gitlab-runner01

  deploy_web_demo:
    stage: deploy
    variables:
      SERVICE_DOMAIN: gitlab-service.example.com
    before_script:
      - docker rm -f ${CONTAINER_NAME} || true
    script:
      - docker run --name ${CONTAINER_NAME} -p 80:8080 -d ${APP_NAME}
      - sleep 3
      - curl http://${SERVICE_DOMAIN}/${APP_NAME}/
    tags:
      - shell
      - production-gitlab-runner01

Dockerfile

FROM tomcat:latest
COPY build/libs/*.war /usr/local/tomcat/webapps
CMD ["catalina.sh", "run"]

うーむ、build, test, deployを書いていくのはわかった。

[GitLab CI/CD Jobs] 基本設定

GitLab CI/CDにおけるジョブは「.gitlab-ci.yml」に定義する

.gitlab-ci.yml

job01:
  script:
    - uname -r
    - ps
    - hostname

Job02:
  script:
    - ./build.sh
    - ./test.sh

– Scriptパラメータ利用例

job01:
  script:
    - uname -a
    - ./scripts/build.sh
    - bundle exec rspec spec/requests/

– only, expectパラメータ

job01:
  only:
    - /^script-.*$/
  except:
    - tags

only, exceptで使用できるオプション
branches, tags, pushes, schedules, web

– variablesパラメータ

job01:
  variables:
    PROXY_PATH: 'http://proxy.example.com'
    FLAGS: '-0'
  script:
    - 'https_proxy=$PROXY_PATH curl $FLAGS https://gitlab.com/gitlab-org/gitlab-ce/repository/archive.tar.gz'

– tagパラメータ
プロジェクト内で実行可能なRunnerのリストから特定のRunnerを選択する際に使用
指定したタグを利用し、それにマッチしたRunnerだけにジョブを渡す

job01:
  tags:
    - shell
    - gitlab-runner01

– artifactsパラメータ
ジョブの実行結果をアーティファクトとして保存する際の定義を行う
name, untracked, when, expire_in

job01:
  artifacts:
    name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}"
    untracked: true
    expire_in: 10 min
    paths:
      - ./artifacts

### pythonを実行するサンプル

job01:
  script:
    - python ./scripts/hello.py
  only:
    - master
  tags:
    - shell
    - gitlab-runner01

.gitlab-ci.ymlでbuildとテストを自動化するところまではわかった。
.gitlab-ci.ymlでDockerのbuildをどうやるかが問題か…

[GitLab] CI(継続的インテグレーション)

継続的CIにはJenkins, Travis CI, Circle CIなどがよく利用される
GitLab CI/CDのジョブ機能を使うことによって、ビルドツールやテストツールと連携したインテグレーションの自動化が可能となる

### GitLab CI/CD Jobs
– マルチプラットフォーム: Go, Linux, Windows, FreeBSD, Dockerなどどのプラットフォーム上でもジョブが実行可
– Merge Requestとの連携: Merge Request後のビルド実行確認ができる
– 並列分散実行: 複数のGitLab runnerプロセスにより並列にビルド実行が可能
– オートスケール: ジョブのオートスケールを実現
– アーティファクトの管理: ブラウザ上から利用可能
L Dockerのレジストリ機能とも簡単に連携できる

### GitLab Runner
GitLab CI/CD上から指示されたスクリプトを実行したり、一時的にDockerコンテナを生成してジョブを実行するプロセス
※コンテナをRunnerする場合は2通りある
1. DockerホストにRunnerをインストールしてコンテナをジョブとして起動
2. DockerコンテナをRunnerとして、コンテナ内でジョブを実行

### Executorの種類
– Shell Executor: Runnerが導入されているサーバ上でビルドやテストを実行できるシンプルなExcecutor
– Docker Executor: Docker APIを通してDocker Engineと接続することによって、コンテナから各ビルド作業を実行
– Virtual Box Executor: VirtualBoxのVMを利用したビルド環境を提供 sshとbashを経由
– SSH Executor: SSH接続可能な特定サーバに対してコマンドをSSH経由で送りつける
– Kubernetes Executor: Kubernetes API経由でクラスタ上のPodを作成してビルドを実施。.gitlab-ci.yml内で定義されたServiceパラメータごとに新たにコンテナが生成されビルド、テストを実施

### Runnerの種類
Shared Runners(共有のRunnerで処理)とSpecific Runners(特定のプロジェクトのジョブのみ実行)の2種類がある
Go言語で作成されており、1つのプロセスで複数のExecutorを登録できる

GitLabの開発フローとコード管理

Merge Requestを利用したレビュープロセス

1. 保護ブランチワークフロー: 開発メンバー全員が1つのGithubプロジェクトを利用
2. フォークワークフロー:管理者やレビューアのみがリポジトリを操作できるようになっており、開発者はプロジェクトReporterアクセス権限もに付与されており、自身のリポジトリ上で開発を行う

ブランチに対して権限を付与することで、対象ブランチの偶発的な更新や削除を防ぐ

ブランチの作成
$ git checkout -b
ブランチ削除
$ git branch -d

$ git add new_feature.py
$ touch new_feature.py
$ git add .
$ git commit -m “[Add]new feature library Ref #1”
$ git push origin develop

### Merge Requestの作成
マージ先にはmasterが指定されている
開発規模の肥大化を避けるためにも小さい単位でのMerge Requestが望まれる。ただし開発の生産性と複雑度に依存する
Merge Requestがマージされると同時にテストの自動化が必要(シンタックスエラー、デプロイエラーなど)

### ブランチ戦略 GitLab Flow
機能ブランチ(Feature Branch)とMasterブランチ(Master Branch)
環境ごとにブランチを作りmasterからpre-production, productionにmerge requestを送る

なるほど〜
開発工程においてbranchの設計って凄い大事なんやな〜

Gitによるチーム開発

1. ブランチの作成
$ git branch
* master
$ git branch develop
$ git branch
develop
* master

2.ブランチの切り替え
$ git checkout develop
Switched to branch ‘develop’
$ echo -e “# GitLab Project\nObjects for GitLab” > README.md
$ cat README.md
# GitLab Project
Objects for GitLab
$ git add -u
$ git commit -m “[Modify]Update title in README.md”
[develop 895daf2] [Modify]Update title in README.md
1 file changed, 2 insertions(+)
$ git log

3.ブランチのマージ
$ git checkout master
$ git merge develop
$ cat README.md
# GitLab Project
Objects for GitLab

リモートリポジトリの活用
-> メインのリポジトリをリモートリポジトリ側に置き、各自ローカルリポジトリの更新をマージしていくことでチーム開発を進めていく

$ git branch -a
develop
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master
$ git remote add gitlab https://gitlab.com/h3377/myproject.git

### リモートリポジトリの反映
$ git fetch gitlab
$ get merge FETCH_HEAD
// fetchとmergeを同時に行えるpullの方が早いが、コンフリクトを起きたときはfetch, mergeで対応する
$ git pull gitlab HEAD

### リモートリポジトリへの送信
$ vi README.md
$ git add -u
$ git commit -m “[UPDATE]Add description for README.md”
[master 79bed9e] [UPDATE]Add description for README.md
1 file changed, 1 insertion(+), 1 deletion(-)
$ git push gitlab HEAD

### Issue Tracker
– Group Issues: グループ共通の課題チケット
– Project Issues: プロジェクトで個別に割り当てられるチケット
– マイルストーンの作成
– Labels: 各課題チケットを分類するためのカテゴリ(bug, confirmed, ciritial, discussion, documentation, enhancement, suggestion, supportなどがデフォルト)
 -> チケット駆動開発(TiDD)

### Issue Board
チケットの優先順位とステータスを管理

[AWS CloudFormation] S3とCloudFront(Origin Access Identityは使わない)

AWSTemplateFormatVersion: "2010-09-09"
Description:
  S3 and CloudFront for Static website hosting

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "S3 and CooudFront Configuration"
        Parameters:
          - WebsiteDomainName
          - CFSSLCertificateId

    ParameterLables:
      WebsiteDomainName:
        default: "WebsiteDomainName"
      CFSSLCertificateId:
        default: "CFSSLCertificateId"

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  WebsiteDomainName:
    Type: String

  CFSSLCertificateId:
    Type: String


Resources:
# ------------------------------------------------------------#
#  S3 Bucket
# ------------------------------------------------------------#        
# Bucket
  Bucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Ref WebsiteDomainName
      AccessControl: PublicRead
      WebsiteConfiguration:
        IndexDocument: index.html

  BucketPolicy:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Ref Bucket
      PolicyDocument:
        Statement:
        - Action: "s3:GetObject"
          Effect: Allow
          Resource: !Sub "arn:aws:s3:::${Bucket}/*"
          Principal: '*'

# ------------------------------------------------------------#
#  CloudFront
# ------------------------------------------------------------#
  CloudFrontDistribution:
    Type: "AWS::CloudFront::Distribution"
    Properties:
      DistributionConfig:
        PriceClass: PriceClass_All
        Aliases:
        - !Ref WebsiteDomainName
        Origins:
        - CustomOriginConfig:
            OriginProtocolPolicy: http-only
          DomainName: !Sub "${WebsiteDomainName}.s3-website-${AWS::Region}.amazonaws.com"
          Id: !Sub "S3-Website-${WebsiteDomainName}.s3-website-ap-northeast-1.amazonaws.com"
        DefaultCacheBehavior:
          TargetOriginId: !Sub "S3-Website-${WebsiteDomainName}.s3-website-ap-northeast-1.amazonaws.com"
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods:
          - GET
          - HEAD
          CachedMethods:
          - GET
          - HEAD
          DefaultTTL: 3600
          MaxTTL: 86400
          MinTTL: 60
          Compress: true
          ForwardedValues:
            Cookies:
              Forward: none
            QueryString: false
        ViewerCertificate:
          SslSupportMethod: sni-only
          MinimumProtocolVersion: TLSv1.1_2016
          AcmCertificateArn: !Sub "arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/${CFSSLCertificateId}"
        HttpVersion: http2
        Enabled: true

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------# 
Outputs:
#WebsiteURL:
  WebsiteURL:
    Value: !GetAtt Bucket.WebsiteURL

#DistributionID
  DistributionID:
    Value: !Ref CloudFrontDistribution

#DomainName
  DomainName:
    Value: !GetAtt CloudFrontDistribution.DomainName

なるほど、これはやばいわ

[AWS CloudFormation] EBSスナップショットのData Lifecycle Managerを設定

Amazon Data Lifecycle Managerを利用すると、バックアップしたいEBSとそのバックアップポリシーを定義するだけで自動的にスナップショットを取得することができる

EBSNameTagを指定する

AWSTemplateFormatVersion: "2010-09-09"
Description:
  Create DLM LifecyclePolicy

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  EBSNameTag:
    Type: String

# ------------------------------------------------------------#
#  DLM LifecyclePolicy
# ------------------------------------------------------------#
Resources:
  LifecyclePolicy:
    Type: "AWS::DLM::LifecyclePolicy"
    Properties:
      Description: "Daily EBS Snapshot"
      State: "ENABLED"
      ExecutionRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/AWSDataLifecycleManagerDefaultRole"
      PolicyDetails:
        ResourceTypes:
          - "VOLUME"
        TargetTags:
          -
            Key: "Name"
            Value: !Ref EBSNameTag

        Schedules:
          -
            Name: !Sub "${EBSNameTag}-daily-snapshot"
            TagsToAdd:
              -
                Key: "type"
                Value: "DailySnapshot"

            CreateRule:
              Interval: 24
              IntervalUnit: "HOURS"
              Times:
                - "17:00"
            RetainRule:
              Count: 3
            CopyTags: true

なるほど、backupもcloudformationで書けちゃうのね…

[AWS CloudFormation] CloudFormationマクロを作成

### CloudFormationマクロとは
CloudFormationテンプレートの標準的な定義だけでは実現できない処理を、テンプレート内からLambda関数(マクロ)を呼び出すことで実現できるようにするCloudFormation拡張機能のこと

AWSTemplateFormatVersion: "2010-09-09"
Description: 
  CloudFormation Macro Create

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  CFnMacroName:
    Type: String

# ------------------------------------------------------------#
#  LambdaExecutionRole
# ------------------------------------------------------------#        
Resources:
  LambdaExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub "${CFnMacroName}-LambdaExecutionRole"
      Policies:
        - PolicyName: !Sub "${CFnMacroName}-LambdaExecutionRole-Policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: "arn:aws:logs:*:*:*"

              - Effect: Allow
                Action:
                  - "ssm:PutParameter"
                Resource: "*"

      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Sid: ""
            Effect: Allow
            Principal: 
              Service: "lambda.amazonaws.com"
            Action: "sts:AssumeRole"

# ------------------------------------------------------------#
#  Lambda Function for CloudFormation Macro
#  example : generate random string(10) and register SSM
# ------------------------------------------------------------#  
  LambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      FunctionName: !Ref CFnMacroName
      Role: !GetAtt LambdaExecutionRole.Arn
      Handler: index.handler

      Code:
        ZipFile: !Sub |
          import boto3
          import string
          import random

          ssm = boto3.client('ssm')

          def handler(event, context):
            key = event['params']['Key']
            description = event['params']['Description']
            randomstr = ''.join(random.choices(string.ascii_letters + string.digits, k=10))

            ssm.put_parameter(
              Name=key,
              Value=randomstr,
              Type='SecureString',
              Description=description
            )
            return {'requestId': event['requestId'], 'status': 'success', 'fragment': randomstr}

      Runtime: "python3.6"
      MemorySize: 128
      Timeout: 5

# ------------------------------------------------------------#
#  CloudFormation Macro
# ------------------------------------------------------------#  
  CFnMacro:
    Type: "AWS::CloudFormation::Macro"
    Properties:
      FunctionName: !Ref LambdaFunction
      Name: !Ref CFnMacroName
      Description: !Ref CFnMacroName 

index.pyが作られる

これは凄いな

[AWS CloudFormation] ELBログ用のS3 Bucket作成

AWSTemplateFormatVersion: "2010-09-09"
Description:
  S3 Bucket for ELB AccessLog Create

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "S3 Bucket for ELB AccessLog Configuration"
        Parameters:
          - ELBLogBucketName

    ParameterLabels:
      ELBLogBucketName:
        default: "ELBLogBucketName"

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  ELBLogBucketName:
    Type: String

# ------------------------------------------------------------#
# ELBAccountId Mappings
# ------------------------------------------------------------# 
Mappings:
  ELBAccountID:
    ap-northeast-1:
      "AccountId": ""

Resources:
# ------------------------------------------------------------#
#  S3
# ------------------------------------------------------------#        
# ELBLogBucket
  ELBLogBucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Ref ELBLogBucketName

#BucketPolicy
  ELBLogBucketPolicy:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Ref ELBLogBucket
      PolicyDocument:
        Id: "AWSCFn-AccessLogs-Policy-20180920"
        Version: "2012-10-17"
        Statement:
          - Sid: AWSCFn-20180920
            Effect: "Allow"
            Action:
              - "s3:PutObject"
            Resource: !Sub "arn:aws:s3:::${ELBLogBucket}/AWSLogs/${AWS::AccountId}/*"
            Principal:
              AWS: !FindInMap [ ELBAccountId, !Ref "AWS::Region", AccountId ]

# ------------------------------------------------------------#                
# Output Parameters
# ------------------------------------------------------------# 
Outputs:
#ELBLogBucket
  ELBLogBucket:
    Value: !Ref ELBLogBucket
    Export:
      Name: !Ref ELBLogBucketName

ELBの管理画面で、AccessLogをS3に設定できるのね。

[AWS CloudFormation] System Managerのパラメータストアの値を登録

System Managerのパラメータストアとは、Key/Valueのパラメータを集中管理する機能

Stringタイプのパラメータを登録する場合のサンプル

AWSTemplateFormatVersion: "2010-09-09"
Description:
  SSM Parameter Create

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  MasterUserName:
    Type: String
    NoEcho: true
    MinLength: 1
    MaxLength: 16
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*"
    ConstraintDescription: "must begin with a letter and contain only alphanumeric characters."

# ------------------------------------------------------------#
#  SSM Parameter
# ------------------------------------------------------------# 
Resources:
  Parameter:
    Type: "AWS::SSM::Parameter"
    Properties:
      Name: "MasterUsername"
      Type: "String"
      Value: !Ref MasterUserName
      Description: "MasterUsername for RDS"

うん、これはよーわからんね。