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 以降では
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:*" } } } ] }
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 DOCKER_PASSWORD: 使用するdocker password DOCKER_USER: 使用するdoker user
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.マージリクエストの承認スコープ設定
※この機能は Premium 版以上で使えるようです。無料版では Approve 必須でマージを制限することができませんでした
Approval rules でブランチごとにルールを作成できる
権限スコープの範囲はユーザー、グループ単位で設定し、かつ承認が必要な人数も設定できる
GitLab コンソール(Settings -> General -> Merge request approvals)
ソースコード
初歩的な利用法として、リポジトリのルートディレクトリに「.gitlab-ci.yml」というファイルを配置することで GitLab の CI/CD を動かすことができます
.gitlab-ci.yml
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 リソースを作成するだけのものを用意しました
cdk/app.py
#!/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()
cdk/stacks/sns_stack.py
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 を構築できてしまうのは魅力の一つですね
他にも色んな機能があるようなので色々試して慣れていきたいと思います