GitLabを使ってAWS CDKのスタックをデプロイするCI/CDを構築してみた

GitLab とは?

Git のホスティングサービスであり、Issue の管理、レビュー、CI/CD などの統一的な機能を持っています。類似のサービスとして GitHub が挙げられます。

今回すること

GitLab に備わっている機能の CI/CD を使って CDK のスタックをデプロイする環境を構築したので備忘録としてポイントを残しておこうと思います

完成イメージ

・開発環境(dev)、検証環境(stg)、本番環境(prod)それぞれに AWS アカウントを用意して環境ごとにデプロイできる

・AWS アクセスキー、シークレットキーは登録せず、OpenID Connect でデプロイを実行させる

・ブランチは dev, main の二つを保護ブランチとして扱い、それぞれのブランチをマージ出来る承認者のスコープは異なる。また、直接の push を防止する

・dev ブランチにマージを実行した際は、開発環境(dev)がデプロイされる

・main ブランチをマージを実行した際は、最初に検証環境(stg)がデプロイされ、その後手動実行で本番環境(prod)がデプロイされる

1.前準備

1-1.対象アカウントにデプロイ用に IAM を設定

< ID プロバイダの追加 >

1.OpenID Connect を選択

2.プロバイダの URL と 対象者に「 https://gitlab.com 」を登録(オンプレミス版の場合はホストの URL に合わせて変更する。末尾に/は含めないので注意)

3.「サムプリント取得」を押下

4.「プロバイダを追加」を押下

AWS コンソール(IAM -> アクセス管理 / ID プロバイダ -> プロバイダを追加)

< OIDC 用の Role の作成 >

IAM ポリシーは、Stack のデプロイに必要な権限を設定したものをアタッチする。

信頼関係は以下のように設定

GITLAB_PROJECT_PATH の例として、

「sample_project/gitlab_cicd_sample」

のように gitlab の対象プロジェクトへのパスを設定

ref_type 以降では

ref_type:{“branch” or “tag”}:ref:{<branch 名> or <tag 名>}

の形式でスコープを絞ることが可能、

今回は feature, dev, main のブランチで共通して使用するので

「ref_type:branch:ref:*」

としている

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::{ AWS_ACCOUNT_ID }:oidc-provider/gitlab.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringLike": {
          "gitlab.com:sub": "project_path:{ GITLAB_PROJECT_PATH }:ref_type:branch:ref:*"
        }
      }
    }
  ]
}

1-2.必要な環境変数を各ステージごとに設定

AWS_DEFAULT_REGION: 対応するリージョン文字列 ex) ap-northeast-1
ROLE_ARN_DEV: dev環境のIAM Role arn
ROLE_ARN_STG: stg環境のIAM Role arn
ROLE_ARN_PROD: prod環境のIAM Role arn

GilLab コンソール(Settings -> CI/CD -> Variables)

1-3.ブランチごとに保護設定、承認ユーザー設定を行う

保護ブランチは共通して Allowed to push の項目を No one にし、Allowed to force push を不活性にする

・main ブランチ

Allowed to merge の欄でマージが出来る Role or Users を設定できる

ここの設定を行うことで、merge request の Approve 後にマージ出来るユーザーの制限と

prod 環境デプロイへの手動実行を行えるユーザーを制限できる

・dev ブランチ

Allowed to merge のスコープを開発者の範囲に設定する

GitLab コンソール(Settings -> Repository -> Protected branches)

1-4.マージリクエストの承認スコープ設定

Approval rules でブランチごとにルールを作成できる

※この機能はPremium版以上で使えるようです。無料版では Approve 必須でマージを制限することができませんでした

権限スコープの範囲はユーザー、グループ単位で設定し、かつ承認が必要な人数も設定できる

GitLab コンソール(Settings -> General -> Merge request approvals)

ソースコード

初歩的な利用法として、リポジトリのルートディレクトリに「.gitlab-ci.yml」というファイルを配置することで GitLab の CI/CD を動かすことができます

stages:
  - deploy
  - deploy/manual

# AWS Assume role
.assume_role: &assume_role
  - STS=$(aws sts assume-role-with-web-identity --role-arn ${ROLE_ARN} --role-session-name "GitLabRunner-${CI_PROJECT_ID}-${CI_PIPELINE_ID}" --web-identity-token $CI_JOB_JWT_V2 --duration-seconds 3600)
  - export AWS_ACCESS_KEY_ID=$(echo "$STS" | jq -r ".Credentials.AccessKeyId")
  - export AWS_SECRET_ACCESS_KEY=$(echo "$STS" | jq -r ".Credentials.SecretAccessKey")
  - export AWS_SESSION_TOKEN=$(echo "$STS" | jq -r ".Credentials.SessionToken")
  - export AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION
  - export AWS_ACCOUNT_ID=$(aws sts get-caller-identity | jq -r ".Account" )

# COMMON SCRIPT
.common_dev_environments: &common_dev_environments
  ENVIRONMENT: dev
  ROLE_ARN: $ROLE_ARN_DEV
  ENV_NAME: Dev

.common_stg_environments: &common_stg_environments
  ENVIRONMENT: stg
  ROLE_ARN: $ROLE_ARN_STG
  ENV_NAME: Stg

.common_prod_environments: &common_prod_environments
  ENVIRONMENT: prod
  ROLE_ARN: $ROLE_ARN_PROD
  ENV_NAME: Prod

# AWS CLI SCRIPT
.aws_cli_install_script: &aws_cli_install_script
  - apk add --no-cache binutils jq
  - curl -sL https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub -o /etc/apk/keys/sgerrand.rsa.pub
  - curl -sLO https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VER}/glibc-${GLIBC_VER}.apk
  - curl -sLO https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VER}/glibc-bin-${GLIBC_VER}.apk
  - curl -sLO https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VER}/glibc-i18n-${GLIBC_VER}.apk
  - apk add --no-cache glibc-${GLIBC_VER}.apk glibc-bin-${GLIBC_VER}.apk glibc-i18n-${GLIBC_VER}.apk
  - /usr/glibc-compat/bin/localedef -i en_US -f UTF-8 en_US.UTF-8
  - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
  - unzip awscliv2.zip -d /opt-aws
  - /opt-aws/aws/install
  - aws --version

.aws_cli_environments: &aws_cli_environments
  GLIBC_VER: 2.31-r0

.common_install_script: &common_install_script
  - echo "http://dl-cdn.alpinelinux.org/alpine/v3.13/main/" >> /etc/apk/repositories
  - apk add --no-cache nodejs npm
  - npm i -g aws-cdk@latest
  - apk add --no-cache python3 gcc libc-dev python3-dev build-base libffi-dev musl-dev cargo libressl-dev curl zip
  - curl https://sh.rustup.rs -sSf | sh -s -- -y
  - source /root/.cargo/env
  - pip3 install -U pip==21.3.1
  - pip3 install -r cdk/requirements.txt
  - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USER --password-stdin

.common_backend_deploy_script: &common_backend_deploy_script
  - cdk bootstrap aws://$AWS_ACCOUNT_ID/$AWS_DEFAULT_REGION
  - cdk -a "python3 app.py" deploy SnsStack$ENV_NAME --require-approval never

.cdk_synth_prod_script: &cdk_synth_prod_script
  - export ENV_NAME=$Prod
  - cdk -a "python3 app.py" synth -o cdk_prod.out -q

.cdk_deploy_prod_script: &cdk_deploy_prod_script
  - cdk bootstrap aws://$AWS_ACCOUNT_ID/$AWS_DEFAULT_REGION
  - cdk -a "cdk_prod.out" deploy SnsStack$ENV_NAME --require-approval never

default:
  image: docker:19.03.1
  services:
    - docker:19.03.5-dind
  before_script:
    - *common_install_script
    - *aws_cli_install_script
    - *assume_role

# job
cdk_dev_deploy_job:
  stage: deploy
  only:
    refs:
      - dev
    changes:
      - cdk/**/*
      - .gitlab-ci.yml
  variables:
    <<: [*aws_cli_environments, *common_dev_environments]
  script:
    - cd cdk
    - *common_backend_deploy_script

cdk_stg_deploy_job:
  stage: deploy
  only:
    refs:
      - main
    changes:
      - cdk/**/*
      - .gitlab-ci.yml
  variables:
    <<: [*aws_cli_environments, *common_stg_environments]
  script:
    - cd cdk
    - *common_backend_deploy_script
    - *cdk_synth_prod_script
  artifacts:
    paths:
      - 'cdk/cdk_prod.out'

cdk_prod_deploy_job:
  stage: deploy/manual
  when: manual
  only:
    refs:
      - main
    changes:
      - cdk/**/*
      - .gitlab-ci.yml
  variables:
    <<: [*aws_cli_environments, *common_prod_environments]
  script:
    - cd cdk
    - *cdk_deploy_prod_script

検証用の Stack は簡単に SNS リソースを作成するだけのものを用意しました

#!/usr/bin/env python3
import os
import aws_cdk as cdk
from stacks.sns_stack import SnsStack

env_name = os.getenv('ENV_NAME', 'Stg')

app = cdk.App()
SnsStack(app, "SnsStack" + env_name, stage=env_name)

app.synth()
from aws_cdk import (
    Stack,
    aws_sns as sns
)
from constructs import Construct


class SnsStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        stage: str = kwargs.pop('stage')
        super().__init__(scope, construct_id, **kwargs)
        stack_prefix = "spike-{}-{}"

        sns_name = stack_prefix.format("sns", stage)
        sns_topic = sns.Topic(self, sns_name,
                              topic_name=sns_name,
                              display_name=sns_name)

        self.sns_topic = sns_topic

ポイント解説

stages:

stages で job の実行順番を決めることが出来ます

以下の例では deploy の job を実行後、deploy/manual が実行されます

stages:
  - deploy
  - deploy/manual

default:

グローバルデフォルトの機能で、すべてのジョブのデフォルトとしてグローバルに設定することができる

今回は image, services, before_script の内容を各 job で共通に使用されるようにしています

default:
  image: docker:19.03.1
  services:
    - docker:19.03.5-dind
  before_script:
    - *common_install_script
    - *aws_cli_install_script
    - *assume_role

job: のプロパティについて

stage: stages で定義した stage を割り当てる

only: refs に検知するブランチ名を記載、 changes にコードの変更検知範囲を記載

when: manual とすることで、job の実行を手動実行されるようにする

variables: job 実行環境の環境変数を設定する

before_script: script 前に実行するスクリプト、事前インストールする必要があるものはここで書く

script: CDK デプロイ実行など job で実行したい処理をここで書く

artifacts の利用について

アーティファクトの機能によって stg のデプロイジョブで作成した cdk_prod.out を prod のデプロイジョブに共有させるようにしました

cdk deploy コマンドで–app のオプションで cdk.out のフォルダを指定すると既に作成されている cdk.out の内容を読み込んでデプロイが実行されます

このことを利用して、検証環境(stg)のジョブと本番環境(prod)のジョブのデプロイ実行で使用する CloudFormation 生成のタイミングを合わせるようにしています

まとめ

GitLab 内のサービスで簡単に CI/CD を構築できてしまうのは魅力の一つですね

他にも色んな機能があるようなので色々試して慣れていきたいと思います

無料相談実施中
AWSを使用したサーバーレスアプリケーションの構築
サーバーレス開発内製化、AWSへの移行等
様々な課題について無料でご相談お受けいたします。
最新情報をチェックしよう!