0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【CICD入門】AWSでシンプルなCICDを作る(第1回:全体像編)

0
Posted at

AWS標準機能だけでLambdaのCI/CDを作る(全体像編)

🐈🐈🐈 作成した内容は👆こちら 🐈🐈🐈

1. はじめに

よく仕事で小さなプログラムをAWS上で定期実行したい、という案件に出会うことがあります。

  • 毎週1回、外部からファイルを取得してS3へ保存する
  • 定期的に外部APIを呼び出してデータを取得する
  • 一定間隔で簡単な集計を行う

Lambdaの中で動かす処理は案件ごとに違いますが、その周りに必要なものはほとんど同じです。実行Role、定期実行、ログ、監視、通知、テスト、CI/CD、Version管理、環境分離。これを案件ごとに設計し直していると、本来書きたい処理へ着手するまでに時間がかかります。

そこで、この共通部分だけを最小構成でテンプレート化しました。3回に分けて、その中身と設計判断を書いていきます。

やりたいこと

  • 新しい案件では、コードを書いてpushすればLambdaが更新される状態から始めたい
  • そのために自分で作り込むコードは、できるだけ少なくしたい
  • 何が起きているか追えない仕組みは入れたくない

この構成のメリット

  • CI/CDの処理をAWS標準機能へ寄せているため、動きを公式ドキュメントで確認できる
  • 自作のスクリプトが少ないので、壊れる箇所が少ない
  • Lambdaを1つ追加する手間が、ディレクトリを1つ増やすだけになる

今回は全体像と設計の方針までを扱います。AWSへの構築は第2回、リリースと監視の仕組みは第3回で扱います。

2. 今回作るもの

全体の構成

GitHubへpushすると、テスト、ビルド、デプロイが順に動いてLambdaが更新されます。

開発者
  │ push
  ▼
GitHub ──> CodePipeline
             ├ Source   GitHubからコードを取得
             ├ Test     pytestを実行
             ├ Build    SAMでLambdaのパッケージを作る
             └ Deploy   CloudFormationでStackを更新
                          │
                          ▼
                        Lambda(定期実行、ログ、監視つき)

Pipelineの実体はCodePipelineとCodeBuildです。デプロイはSAM(AWS Serverless Application Model)のコマンドをCodeBuildから実行します。自作のデプロイスクリプトは、設定値を組み立ててSAMへ渡す部分だけです。

3つのStack

CloudFormationのStackを、変更の頻度で3つに分けています。

Stack 中身 誰が更新するか 更新の頻度
Bootstrap CloudFormationが使うIAM Role、SAMの資材置き場のS3 Bucket 管理者 ほぼ変更しない
CI CodePipeline、CodeBuild、GitHub接続、通知用のSNS Topic 管理者 CI構成を変えるときだけ
Runtime Lambda本体、実行Role、Log Group、Alarm、定期実行のScheduler Pipeline コード変更のたび

Pipelineが触るのはRuntime Stackだけです。Pipeline自身の定義(CI Stack)や権限の定義(Bootstrap Stack)は、Pipelineから更新させません。Pipelineが壊れたときに、Pipelineを使って直す状況を避けるためです。

使用技術

分類 使うもの
IaC CloudFormation、AWS SAM
CI/CD CodePipeline、CodeBuild、CodeConnections(GitHub接続)、CodeDeploy
実行基盤 Lambda(Python 3.12)、EventBridge Scheduler
監視・通知 CloudWatch Alarm、CloudWatch Logs、SNS、EventBridge Rule
開発 Python 3.12、pytest、PyYAML

新しいサービスは使っていません。組み合わせ方だけを決めたものが、このテンプレートです。

3. 設計の方針

標準機能に寄せる

CI/CDを自分で作り始めると、だんだん「自作の仕組み」が増えていきます。デプロイ対象を記録するファイル、前回成功したコミットとの差分を見る処理、失敗したときに戻すスクリプト。どれも最初は便利ですが、増えるほどその仕組み自身の面倒を見る必要が出てきます。

そこで、判断の基準を1つ決めました。

標準機能で説明できない仕組みは作らない。

持たない仕組み

意図的に作らなかったものを、最初に書いておきます。

持たないもの 代わりにどうするか
Git差分による対象Lambdaの選別 毎回すべてのStackを処理し、変更の有無はCloudFormationに判断させる
デプロイ対象の管理ファイル ディレクトリの配置がそのまま対象一覧になる
ZIP作成、Version発行、Alias更新のスクリプト SAMのビルドとAutoPublishAliasに任せる
Rollbackスクリプト CloudFormationとCodeDeployの標準動作に任せる
Lambda Layer 共有コードを各Lambdaのパッケージへ同梱する
Nested Stack 使わない。必要性が確認できた時点で判断する

「持たないもの」を先に決めると、設計が安定します。迷ったときに「それは持たない側だ」と判断できるためです。

毎回すべて配備する

このテンプレートは、Lambdaが10個あればデプロイのたびに10個すべてのStackを処理します。差分を見て絞り込みません。

「無駄では」と思われるところですが、実際に無駄なリリースは起きません。CloudFormationが変更の有無を判断するためです。

期待する挙動 どう実現されるか
テストだけを直したときにLambdaを更新しない コードが変わらないので、Change Setが空になる
関係ない設定だけを変えたときにVersionを増やさない 変更されたResourceだけが更新される
1つのLambdaを直したときに他へ影響しない Stackが分かれているため、他StackのChange Setは空になる

Change Setは、CloudFormationが「今の状態と比べて何を変えるか」をまとめた変更計画です。中身が空なら何も起きません。

代わりに失うものもあります。Lambdaの数が増えると、デプロイ時間が全体の数に比例して伸びます。この構成はLambdaが10〜15個程度までを想定しています。

4. 1つのStackに何を入れるか

1 Runtime Stack = 1つのデプロイ単位

Runtime Stackは「まとめて配備したい範囲」を1つにしたものです。単独のLambdaであれば、こうなります。

Runtime Stack: example-project-dev-daily-report
│
├ Scheduler          定期実行(EventBridge Scheduler)
│  └ Scheduler Role  このLambdaを呼ぶ権限だけ
│        │ 呼び出し
│        ▼
├ Lambda
│  ├ live Alias      呼び出し先はここに固定
│  ├ Version         デプロイのたびに発行
│  └ 実行Role        自分のLog Groupへ書く権限だけ
│        │ 出力
│        ▼
├ Log Group          保持期間を指定。Stack削除後も残す
│
└ Alarm × 2          エラー数と実行時間

Lambdaだけを別Stackにして、Schedulerやログを共通Stackへ置く構成も考えられます。そうしなかった理由は2つあります。

理由 内容
追いやすさ 1つのTemplateを読めば、その処理に関わるResourceが全部分かる
参照の制約 Stackをまたいで値を渡すと、参照されている間はそのStackを作り直せなくなる

名前の付け方

Stack名は<Project名>-<環境>-<Runtime名>に固定します。この例ならexample-project-dev-daily-reportです。

その中のResource名も、Stack名を土台に付けます。

Resource 名前
Lambda関数 example-project-dev-daily-report
実行Role example-project-dev-daily-report-execution
Alarm example-project-dev-daily-report-errors

名前を固定しているのは、IAMで操作範囲を絞るためです。「example-project-dev-*という名前のLambdaだけ作れる」という許可の書き方ができます。ここは第2回で詳しく書きます。

5. ディレクトリ全体

repository/
├ application/                   # Lambdaの実行コードとテスト
│  ├ common/                     # 全Lambdaへ同梱する共有コード
│  │  ├ logger.py
│  │  └ utils.py
│  ├ functions/
│  │  └ daily_report/
│  │     ├ handler.py            # Lambdaの入口
│  │     └ requirements.txt      # この関数が使う外部ライブラリ
│  └ tests/                      # pytestのテスト
├ infrastructure/                # Template(設計)と設定値
│  ├ bootstrap/template.yaml
│  ├ ci/template.yaml
│  ├ runtimes/
│  │  └ daily-report/
│  │     ├ template.yaml         # Runtime 1つ分
│  │     └ parameters.json       # このRuntimeの設定値
│  ├ env/
│  │  ├ dev/
│  │  ├ stg/
│  │  └ prod/                    # 環境ごとの設定値
│  └ tests/                      # Templateの構造テスト
└ deployment/                     # Test / Build / Deployの補助処理
   ├ buildspecs/
   └ scripts/

infrastructure/runtimes/のディレクトリ名が、そのままRuntime名になります。別途一覧ファイルを持ちません。 ディレクトリを置くことが「デプロイ対象である」という意味になります。

日常的に触るのはこの2か所です。

やりたいこと 触る場所
処理を書く・直す application/functions/<関数名>/handler.py
実行間隔を変える infrastructure/runtimes/<Runtime名>/parameters.json

6. 手元で動かす

このテンプレートは、AWSのアカウントがなくてもテストまで動きます。必要なのはPython 3.12だけです。

python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-dev.txt
python -m pytest

SAM CLIもDockerもAWSの認証情報も要りません。CI/CDを組むとき、手元で確認できる範囲を広く取っておくと開発が楽になります。

設定値が正しく組み立てられるかも、AWSへ接続せずに確認できます。

python -m deployment.scripts.parameters --environment dev
daily-report: parameters valid (7)

この確認処理は、Templateが宣言しているParameterと、JSONに書いた設定値を突き合わせます。Templateに無いキーを書いていれば、ここでエラーになります。デプロイ中に気付くより、手元で気付いたほうが早いためです。

なお、値そのものは表示しません。件数だけを出します。CIのログに設定値が残ることを避けるためです。

7. つまずいたところ

S3 Bucketの名前が途中で切れる

構築して最初に気付いた問題です。作られたBucketの名前がこうなっていました。

example-project-dev-bootst-deploymentbucket-f2je2bt672l3

bootstrapがbootstで切れています。

CloudFormationは、名前を指定しなかったResourceに<Stack名>-<論理ID>-<ランダム>という名前を付けます。S3 Bucketの名前は63文字までなので、超えた分は途中が切り詰められます。

要素 文字数
Stack名(example-project-dev-bootstrap) 29
論理ID(DeploymentBucket) 16
ランダム部分とハイフン 15
合計 60

Project名が長くなると、すぐに63文字を超えます。名前が切れると、名前でResourceを判別できなくなり、名前で許可範囲を絞っているIAMからも外れる恐れがあります。

対処は2つです。

  1. 論理IDを短くする(DeploymentBucket → DeployBucket)
  2. Project名の上限を20文字に決める

そして、二度と起きないようにテストを書きました。Project名とRuntime名を上限いっぱいにした最長のStack名で、すべてのResource名が上限に収まるかを確認します。

# Project名を上限の20文字にした、最も長くなるStack名で確認する
PROJECT = "p" * 20
ENVIRONMENT = "prod"

def test_resource_names_fit_their_limits():
    stack = f"{PROJECT}-{ENVIRONMENT}-{runtime_name}"
    for logical_id, resource in resources.items():
        # 名前を指定しないResourceは <Stack名>-<論理ID>-<ランダム13文字>
        assert generated_length(stack, logical_id) <= limit_of(resource)

こうした「名前の長さ」は設計の地味な部分ですが、あとから直すとResourceの作り直しになります。最初にテストで縛っておくと安心です。

8. おわりに

今回は全体像と、設計の方針を書きました。

改めて並べると、決めたことは次の3つだけです。

  • 標準機能で説明できない仕組みは作らない
  • 1つのStackに、まとめて配備したい範囲を入れる
  • 名前はStack名から決める

次回は、実際にAWSへBootstrap StackとCI Stackを作り、pushでLambdaが配備される状態まで進めます。CI/CDで最初につまずきやすいIAMの権限設計を、2つのRoleの役割分担という形で整理します。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?