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

#GitLab
#CI/CD
#承認フロー
#AWS CDK

Git のホスティングサービスであり、Issue の管理、レビュー、CI/CD などの統一的な機能を持っています

類似のサービスとして GitHub が挙げられます

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

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

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

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

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

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

ブランチ図

< ID プロバイダの追加 >

1.OpenID Connect を選択

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

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

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

プロバイダの追加

< OIDC 用の Role の作成 >

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

信頼関係は以下のように設定 GITLAB_PROJECT_PATH の例として、

「sample_project/gitlab_cicd_sample」

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

ref_type 以降では

reftype:{"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:*" } } } ] }
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 DOCKER_PASSWORD: 使用するdocker password DOCKER_USER: 使用するdoker user

variables

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

  • ・main ブランチ

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

ここの設定を行うことで、merge request の Approve 後にマージ出来るユーザーの制限と prod 環境デプロイへの手動実行を行えるユーザーを制限できる

  • ・dev ブランチ

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

protect_branch

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

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

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

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 で job の実行順番を決めることが出来ます

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

stages: - deploy - deploy/manual

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

今回は 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

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

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

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

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

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

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

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

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

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

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

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