はじめに

GitHub Actionsを利用したAWS CloudFormation(以後CFnと記載)のデプロイを行う方法について実行手順と共に記載します。

概要

本記事で実装するアーキテクチャを実装することでDevOpsのメリット(生産性の向上)を保ちつつ、

IaC化をしたからこそ発生する以下のようなオペレーションミスの発生を軽減することができます。

  • 作業担当者が変更セット作成せずにスタックを更新してしまい、予期せぬAWSリソースの更新や削除が発生した。
  • スタック更新の際に、読み込ませるファイルを誤ってデプロイしてしまい既存の構成が消えてしまった。
  • AWSコンソール操作の際に誤って「スタックの削除」をクリックしてしまい、スタックが消えてしまった。

システム構成図

GitHubEnterpriseを利用しておりAWS上にコンテナでSelf-hosted Runnerを構築しています。

GitHubEnterpriseでは「IP制限」の設定が可能となります。セキュリティのためその設定をした上でのシステム構成としています。

AWSアカウントを環境毎に分けて各ブランチをマージすることによりデプロイする構成としております。

  • developブランチ:開発環境用AWSアカウントに反映
  • releaseブランチ:ステージング用AWSアカウントに反映
  • mainブランチ:本番環境用AWSアカウントに反映

リリース運用概要

本記事のリリース運用の狙いとしては、開発者が作成したコードをCFn有識者が確認を行い、承認を必須にすることです。

承認者がコード変更内容及び変更セットを確認して、想定外の更新であれば差し戻すことが可能となります。

GitHub上で必要な権限をもった開発者及び承認者のグループ(Team)を作り、リポジトリにブランチ保護設定を行う必要があります。

GitHub手順の流れはVincent Driessen氏がブログに書いた”A successful Git branching model” のブランチモデルに準拠しています。

リリース運用手順

今回は開発環境の更新を実際にやってみた手順を記載します。

開発環境のリリース運用手順は以下の通りです。

◆開発環境リリース運用手順

①developブランチからfeatureブランチを切り出して作成

②CFnコードを修正してfeatureブランチへpush

③featureブランチからdevelopブランチへマージを行うためプルリクエストを作成して承認者へレビュー依頼を行い承認

④featureブランチからdevelopブランチにマージ後にGitHub Actions起動

⑤Self-hosted RunnerからS3へCFnテンプレートのUPLOADとCFnの変更セットの自動作成

⑥変更セットの変更情報が問題ないことを確認の上、GitHub Actionsの再実行

⑦開発環境へデプロイ

 

参考:ステージング環境及び本番環境のリリースについて

ステージング環境や本番環境のデプロイは開発環境と同じ流れで以下の対象ブランチへマージする事で各環境へデプロイできます。

・ステージング環境のデプロイ:developブランチ→releaseブランチ

・本番環境のデプロイ:releaseブランチ→mainブランチ

ただし、本番環境については担当者が許可なくGitHub Actionを起動させないように、

GitHubのEnvironment protection rules設定でGitHub Actions実行及び再実行のタイミングで承認を必須とする構成にしています。

 

作業内容前提

本記事で扱うコードを記載します。

各環境同一の内容でデプロイできるようにCFnテンプレートやGitHub Actionsワークフローファイルは、

1つのファイルで共通になるように作成しています。(IaCのメリットを享受する為)

環境固有で変わる部分はCFnテンプレートのParametersで吸収するようにする作りにしています。

◆CFnテンプレート

AWSTemplateFormatVersion: "2010-09-09"
Description: CloudWatchLogs
Parameters:
  env:
    Description: |
      (Require) Select Environment.
    Type: String
    AllowedValues:
      - dev
      - stg
      - prd
    Default: dev
  retentionindays:
    Description: |
      (Require) CloudWatchLogs Retention in Dayw.
    Type: String
    Default: "90"
Resources:
  Logs:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub ${env}-testlog
      RetentionInDays: !Ref "retentionindays"
 

◆CFnテンプレート(Parameters)※各環境固有

開発環境用

 [
  {
    "ParameterKey": "env",
    "ParameterValue": "dev"
  },
  {
    "ParameterKey": "retentionindays",
    "ParameterValue": "90"
  }
]

ステージング環境用

 [
  {
    "ParameterKey": "env",
    "ParameterValue": "stg"
  },
  {
    "ParameterKey": "retentionindays",
    "ParameterValue": "90"
  }
]

本番環境用

 [
  {
    "ParameterKey": "env",
    "ParameterValue": "prd"
  },
  {
    "ParameterKey": "retentionindays",
    "ParameterValue": "3653"
  }
]

◆GitHub Actionsワークフローファイル

# GitHub Actionsのワークフローの名前
name: Cfn Deploy

# GitHub Actions の起動条件
# - 記載のブランチとファイル名がpushされた場合にGitHub Actionsを実行する。
#   ※マージ後にマージ先のpushイベントが動くためpushを指定
on:
  push:
    branches:
      - develop
      - main
      - release
    paths:
      - .github/workflows/*.yaml
      - "*.yaml"
      - "*.json"

# デプロイ先のCFn情報
env:
  STACK_NAME: Stack-Name #CFnスタック名
  YAML_NAME: test.yaml #CFnテンプレート
  JSON_FILE: test-parameter.json #CFnの入力パラメータ

permissions:
  id-token: write
  contents: read
  packages: read

jobs:
  deploy:
    # ブランチによって実行先の各環境のSelf-hosted Runnerを指定。
    # 実行環境はセルフホストランナーにラベルを付けて判断させている。
    #  - 本番環境:mainブランチ prdラベル
    #  - ステージング環境:releaseブランチ stgラベル
    #  - 開発環境:developブランチ devラベル
    #  - その他:featureブランチなど devラベル
    runs-on: >-
      ${{ 
        github.ref == 'refs/heads/main'         && fromJSON('[ "self-hosted", "prd" ]') ||
        github.ref == 'refs/heads/release'      && fromJSON('[ "self-hosted", "stg" ]') ||
        fromJSON('[ "self-hosted", "dev" ]')
      }}
    # GitHub Actionsのタイムアウト(分)
    timeout-minutes: 40
    # 各環境固有の情報をGitHubのEnvironmentに格納している。
    #  例:AWSアカウントID、S3情報、IAMロール、環境名(dev,prd,stg)など
    #     本コードでは以下のように定義しています。
    #      - production(本番)、staging(ステージング)、development(開発)の3つのEnvironmentsを作成
    #      - secrets.AWS_ROLEARN・・・IAMロールを指定
    #      - secrets.AWS_CFN_BUCKETNAME・・・CFnテンプレートを配置するバケット名
    #      - secrets.AWS_ENV・・・環境名(dev,prd,stg)
    environment:
      name: >-
        ${{
          github.ref == 'refs/heads/main'         && 'production' ||
          github.ref == 'refs/heads/release'      && 'staging'    ||
          'development'
        }}
    steps:
      - name: GitHub Event Context Info
        run: |
          echo "github.event_name=${{github.event_name}}"
          echo "BRACHE : github.ref=${{github.ref}}"
      # Self-hosted RunnerにGitHubリポジトリのファイルをコピー
      - name: Checkout
        uses: actions/checkout@v2

      # Self-hosted Runnerに環境変数を定義
      - name: Destination settings
        env:
          TZ: "Asia/Tokyo" # 時刻タイムゾーン変更
        run: |
          # GitHub Actionsの実行回数
          echo "RUN_NO=${{github.run_attempt}}"             >> $GITHUB_ENV
          # 現在時刻の取得
          echo "CURRENT_DATETIME=$(date +'%Y%m%d-%H%M%S')"  >> $GITHUB_ENV
          echo "Github Actions Run Count : ${{github.run_attempt}}"
      # AWSのIAMロール認証
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{secrets.AWS_ROLEARN}}
          aws-region: ap-northeast-1

      # S3にCFnテンプレートなどをコピー
      - name: S3 Bucket File Upload
        run: |
          aws s3 cp ./${{env.YAML_NAME}} s3://${{secrets.AWS_CFN_BUCKETNAME}}/

      # CFnの変更セットを実行
      - name: Create Change Set ${{env.YAML_NAME}}
        run: >
          aws cloudformation create-change-set --stack-name ${{env.STACK_NAME}} 
          --change-set-name "GitHubActions-${{env.CURRENT_DATETIME}}" 
          --template-url  https://${{secrets.AWS_CFN_BUCKETNAME}}.s3.ap-northeast-1.amazonaws.com/${{env.YAML_NAME}}
          --parameters file://${{secrets.AWS_ENV}}-${{env.JSON_FILE}} --capabilities CAPABILITY_NAMED_IAM

      # CFnの変更セットIDを取得
      - name: Change Set ID Get ${{env.YAML_NAME}}
        run: >
          echo "CHANGESETID=$(aws cloudformation list-change-sets --stack-name ${{env.STACK_NAME}} --output yaml | 
          grep changeSet/GitHubActions-${{env.CURRENT_DATETIME}} | 
          awk '{print $3}')"  >> ${GITHUB_ENV}

      # CFnの変更セット実行結果を取得
      # - CFn変更セットの作成に成功すれば正常終了
      # - CFnに変更が何もなければ正常終了
      # - CFn変更セットの作成に失敗すれば異常終了で後続のデプロイは実行しない
      - name: Change Set Check ${{env.YAML_NAME}}
        run: |
          while true Hub Actionsを再実行した場合にはデプロイを実行
      - name: Cfn Deploy ${{env.YAML_NAME}}
        if: env.RUN_NO != '1'  && env.STATUS == 'CREATE_COMPLETE'
        run: aws cloudformation execute-change-set --change-set-name ${{env.CHANGESETID}}

      # CFnのデプロイ結果確認
      #  - UPDATE-COMPLATEの時だけ正常終了
      #  - 上記以外のステータスの場合は異常終了
      - name: Cfn Deploy Check ${{env.YAML_NAME}}
        if: env.RUN_NO != '1'  && env.STATUS == 'CREATE_COMPLETE'
        run: aws cloudformation wait stack-update-complete --stack-name ${{env.STACK_NAME}}

リリース運用手順の実施

手順①~⑦までを画面付きでご説明します。

◆リリース運用手順(開発環境)

①developブランチからfeatureブランチを切り出して作成

②CFnコードを修正してfeatureブランチへpush

③featureブランチからdevelopブランチへマージを行うためプルリクエストを作成して承認者へレビュー依頼を行い承認

④featureブランチからdevelopブランチにマージ後にGitHub Actions起動

⑤Self-hosted RunnerからCFnの変更セットの自動作成

⑥変更セットの変更情報が問題ないことを確認の上、GitHub Actionsの再実行

⑦開発環境へデプロイ

手順①ブランチの切り出し

手順②CFnコードを修正してpush

◆ファイル修正

[
  {
    "ParameterKey": "env",
    "ParameterValue": "dev"
  },
  {
    "ParameterKey": "retentionindays",
    "ParameterValue": "1"   ←90から1に変更
  }
]

◆push

root@1922c7e1be9d:/infra/cfn-deploy# git add .
root@1922c7e1be9d:/infra/cfn-deploy# git commit -m "Modify retentionindays"
[feature 6149f9d] Modify retentionindays
 1 file changed, 1 insertion(+), 1 deletion(-)
root@1922c7e1be9d:/infra/cfn-deploy# git push origin feature
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 304 bytes | 304.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/XXXXXXX/cfn-deploy
   35cbf43..6149f9d  feature -> feature
root@1922c7e1be9d:/infra/cfn-deploy# 

手順③プルリクエスト作成と承認

GitHubのfeatureブランチからdevelopブランチへマージを行うためプルリクエストを作成して、

承認者に承認を貰います。(承認は割愛します。)

手順④GitHub Actionsの起動

ブランチのマージ完了後にGitHub Actionsが自動起動します。

・マージ完了

・GitHub Actionsの起動

GitHub Actionsワークフローファイルに記載した内容がSelf-hosted Runner上で動作します。

※本番環境の場合はGitHub Actions実行にも承認フローが必要となります。承認されないと動作しません。

参考:GitHub Actionsの実行完了

全ての処理に成功するとこのように表示され、実行が完了します。

手順⑤CFn変更セットの自動作成

GitHub Actionが実行されると、GitHub Actionsワークフローファイルの流れで処理が実行されます。

GitHub Actionsワークフローの大まかな流れとしては以下の通りです。

  1. ブランチ名より開発、STG、本番環境の判定
  2. 環境特有設定(Gitbhubのリポジトリに設定しているEnvironments設定)を適用
  3. 指定された環境のSelf-hosted Runner上にGitHubリポジトリ上のファイルを送付
  4. AWSのIAMロール構成(OIDC認証)
  5. S3上にCFnテンプレートを送付
  6. 変更セット作成(S3上のCFnテンプレートファイルを読み込み)
  7. 変更セットの実行結果を確認
  8. CFnデプロイを実行(※デプロイはGitHub Actions再実行の場合のみ動きます)

CFnの変更セットが今回の更新内容が想定通りかを確認することができます。

・GitHub Actionsの実行詳細

下記図の青枠部分で変更セット作成が正常終了していることがわかります。

初回実行であれば赤枠部分はスキップされており、CFnのデプロイがされていないことがわかります。

・変更セット作成の詳細ログ

Self-hosted Runnerで変更セット作成のAWSCLIが実行され、今回変更対象のCloudWatchLogsが出力されています。

※変更がない場合は変更対象が出力しません。そのままGitHub Actionsは正常終了します。
また、異常終了のステータスになればGitHub Actionsは異常終了します。

・参考:AWS上の変更セット

GitHub上の出力と同様に今回変更対象のCloudWatchLogsが出力されています。

手順⑥GitHub Actionsの再実行

更新内容が問題なければ、GitHub Actionsを再実行します。

※本番環境の場合はGitHub Actions再実行にも承認フローが必要となります。承認されないと動作しません。

・GitHub Actions再実行

※Latest#2と画面表示されていれば2回目の実行です。

手順⑦開発環境へのデプロイ

・GitHub Actions再実行完了

1回目では実行されなかった青枠部分のCFnのデプロイが実行されていることがわかります。

・CFnデプロイの詳細ログ

AWSCLIコマンドでデプロイを行い、CFnスタックのステータスが正常終了になることを確認しています。

※CFnスタックが異常終了のステータスになればGitHub Actionsは異常終了します。

・AWS上のデプロイ確認

CFnデプロイは正常終了しており、CloudWatchLogsの保管期間も90日から1日に変更されました。

まとめ

変更セットを利用して問題なければデプロイを行う方法を今回記載しました。

CI/CDは即座にテストとデプロイすることで生産性を高めることができますが、

大規模システムや業務にとって重要なシステムの更新は承認フローが重要となります。

本記事の内容に準拠することで、然るべきタイミングでの承認と変更セットをチェックした上でデプロイする流れを、

システムで制御することにより、安全に稼働中システムへの反映を行うことができます。

プロジェクトの体制によっては承認者の追加や開発手順やリリース運用手順の流れは異なるので、

そのプロジェクトに合わせてカスタマイズすることが重要になります。

これからDevOpsのアーキテクチャを検討している方や既に実施している方に本記事が少しでも参考になれば幸いです。

 

参考記事