コンテナのビルドとデプロイを自動化しよう

このハンズオンでは、GitHub ActionsからAWS ECS/Fargateへのコンテナイメージの自動ビルドと自動デプロイを、実際にハンズオン形式で手を動かしながら体験します。

  • サンプルリポジトリの準備
  • OIDC連携によるAWSとの安全な接続設定
  • ECRへのイメージビルド・プッシュの自動化
  • ECS Fargateへの自動デプロイ設定
  • 初回デプロイの動作確認と更新デプロイ

1. 事前準備

Gitの基礎知識を習得していることを前提とします。自信のない方は先に以下の講座を実施してください。

この講座のハンズオンでは、以下のツールやアカウントが必要です。まだ準備できていない場合は、リンク先の手順に沿って準備をお願いします。

また、CI/CDパイプラインの前段となる静的解析・自動テストの自動化については、静的解析と自動テストを自動化しようで扱っています。先にそちらを体験しておくと、CI/CDパイプラインの全体像がより掴みやすくなります。

2. ハンズオンの概要

ここでは、実際にコンテナをベースとした自動デプロイのパイプラインを構築するハンズオンを実施します。

まずはハンズオンの概要について説明します。今回は、Python(FastAPI)のアプリケーションをコンテナ化し、Amazon ECS/Fargateへ自動デプロイするCI/CDパイプラインを構築します。

処理の流れとして、開発者がPull Requestをmainブランチにマージすると、GitHub Actionsのワークフローが起動します。このワークフローでは、ビルド工程が最初に実行され、その後デプロイ工程が直列で実行されます。ビルドで作成した成果物(コンテナイメージ)をデプロイで使用するため、直列で行う必要があります。

また、デプロイ時には新しいタスクを起動してヘルスチェックが成功してから古いタスクを停止する仕組みになっているため、サービスを止めずに新しいバージョンへ切り替えることができます。これにより、利用者に影響を与えずに本番環境を更新できます。

合わせて、AWSとの接続はOIDC接続を利用することで、安全に接続を行います。

以上の流れを通して、自動ビルドと自動デプロイによってリリースを自動化するCI/CDパイプラインの全体像を体験できます。

2.1 AWS構成の確認

今回扱うAWSの構成を確認します。今回のハンズオンでは、AWSアカウント内に仮想ネットワークであるVPCとサブネットを作成し、その中にFargateで動作するECSを構築します。このECSが、GitHub Actionsからコンテナをデプロイする対象となります。

次に、インターネット経由でECSへアクセスできるようにするため、Internet Gatewayを用意します。これをVPCに関連付けることで、ユーザはインターネットゲートウェイを経由してECSにアクセスできるようになります。

さらに、コンテナイメージを格納するECRを作成します。GitHub ActionsからECRにイメージをアップロードし、そのイメージをもとにECSでコンテナが起動します。これが、今回の構成全体の流れになります。

3. リポジトリの準備

CI/CDパイプラインを構築するためには、まず対象となるアプリケーションのソースコードを管理するGitリポジトリが必要です。GitHub Actionsはリポジトリに格納されたコードに対して動作するため、ここでは自身のGitHubアカウントにリポジトリを用意し、ローカル環境でコードを編集できる状態を整えます。

3.1 GitHubで空のリポジトリを作成

今回のハンズオンではFastAPIで作成済みのサンプルAPIをCI/CDパイプラインの対象とします。ゼロからアプリケーションを作るとCI/CDの学習に集中できないため、準備済みのサンプルコードを使う形で進めます。

実務に近い形を体験するために、ご自身のGitHubアカウントで新しいリポジトリを作成し、そこにサンプルコードを配置する流れで進めます。自分が作ったリポジトリに対してCI/CDパイプラインを構築することで、実感が湧きやすい体験ができます。

GitHubにログインし、右上の+ボタンからNew repositoryをクリックします。

以下の設定でリポジトリを作成します。

設定項目 設定の基準
Owner ご自身のGitHubアカウント 自分のアカウントで管理するため
Repository name devopscamp-cicd-handson ハンズオン用のリポジトリ名
Visibility Public GitHub Actionsの無料実行時間を気にせず使えるため
💡 ポイント
リポジトリ名は任意ですが、後のOIDC設定でこのリポジトリ名を使うため、覚えておきやすい名前にしておきましょう。本ハンズオンではdevopscamp-cicd-handsonという名前で進めます。

Create repositoryボタンをクリックしてリポジトリを作成します。

作成後、「Quick setup」の画面が表示されたら、ここでは何も操作せずに次のステップに進みます。

3.2 サンプルコードのダウンロード

リモートリポジトリの器ができたので、次はそこに配置するアプリケーションのソースコードを準備します。今回はFastAPIで作成済みのシンプルなAPIを利用するため、以下のボタンからサンプルコードのZIPファイルをダウンロードしてください。

ダウンロードしたZIPファイルを、任意の場所に解凍してください。解凍するとdevopscamp-cicd-handsonという名前のフォルダができます。このフォルダの中身は以下のようになっています。

devopscamp-cicd-handson/
├── Dockerfile
├── requirements.txt
└── main.py

各ファイルの役割を以下の表にまとめます。

ファイル名 役割
main.py FastAPIアプリケーションのエントリーポイント。ヘルスチェック用のエンドポイントと、動作確認用のメッセージを返すエンドポイントが記述されている
requirements.txt Pythonプロジェクトで利用するパッケージの一覧を記載したファイル。pip installでこれらをインストールする
Dockerfile コンテナイメージを作成するための手順を記述した設定ファイル。自動ビルドの際にはこの内容に従ってビルドが実行される

main.pyの中身は以下のとおりで、ヘルスチェック用の/healthと、動作確認用の/の2つのエンドポイントだけを持つシンプルな構成です。

from fastapi import FastAPI

app = FastAPI()


@app.get("/health")
def health():
    return {"status": "ok"}


@app.get("/")
def root():
    return {"message": "API呼び出しテストに成功しました"}

今回のハンズオンではCI/CDパイプラインの構築が主題のため、APIの中身はあえて最小限にしています。のちほどこのメッセージを書き換えてPushすることで、自動デプロイが動いてアプリが更新される様子を体験します。

3.3 Gitリポジトリとして初期化

ダウンロードしたサンプルコードは、まだただのフォルダであり、Gitの管理下に入っていません。このままではGitHubへPushできないため、devopscamp-cicd-handsonフォルダをGitリポジトリとして初期化し、最初のコミットを作成します。

Visual Studio Codeでdevopscamp-cicd-handsonフォルダを開き、ターミナルを開いて以下のコマンドを実行します。

まず、フォルダをGitリポジトリとして初期化します。

git init

以下のような実行結果が表示されます。

Initialized empty Git repository in /path/to/devopscamp-cicd-handson/.git/

次に、すべてのファイルをステージングします。

git add .

ステージングしたファイルをコミットします。

git commit -m "initial commit"

git logなどで初回コミットが作成されていれば、ここまでの操作は完了です。

3.4 リモートリポジトリの登録とPush

ローカルでコミットを作成しただけでは、GitHub上のリモートリポジトリにはまだ何も反映されていません。CI/CDパイプラインはGitHub上のコードに対して動作するため、ローカルリポジトリと先ほど作成したGitHubリポジトリを紐付け、Pushしてコードを反映させます。

まず、ローカルのブランチ名をmainに統一します。Gitのバージョンによっては初期ブランチ名がmasterになっていることがあるため、念のため明示的に変更します。

git branch -M main

次に、先ほど作成したGitHubリポジトリをリモートリポジトリとして登録します。<your-github-account-name>の部分はご自身のGitHubアカウント名に置き換えてください。

git remote add origin https://github.com/<your-github-account-name>/devopscamp-cicd-handson.git
📝 git remoteとは
git remoteは、ローカルリポジトリと連携するリモートリポジトリを管理するコマンドです。originはリモートリポジトリに付ける慣習的な名前で、これ以降git push origin mainのようにoriginという名前でGitHubのリポジトリを指し示せるようになります。

最後に、コミットしたコードをリモートリポジトリにPushします。

git push -u origin main

-uオプションを付けることで、以降はgit pushだけで同じブランチにPushできるようになります。

GitHubのリポジトリページをブラウザで開き、main.pyDockerfileなどのファイルが表示されていれば、ここまでの操作は完了です。

4. AWSリソースの作成

ソースコードの準備ができたので、次はデプロイ先となるAWSリソースを用意します。CI/CDパイプラインは「コードをビルドしてどこかにデプロイする」という流れなので、デプロイ先のインフラが存在しないと動作確認ができません。

今回利用するAWSリソース(VPC、ECR、ECSクラスタなど)はCloudFormationテンプレートにより一括で作成します。本講座の主軸はCI/CDパイプラインの構築であり、AWSインフラの構築自体は本質ではないためです。なお、CI/CDパイプラインで重要な「GitHub ActionsからAWSへ接続するためのIAMロール」は、後の自動デプロイのセクションで手動で作成します。IAMロールの信頼関係や権限の設定は今回のハンズオンの重要な学びの一つであるため、あえて手動で体験してもらう形にしています。

📝 CloudFormationとは
CloudFormationは、AWSのリソースをテンプレート(YAML/JSON)で定義し、自動的に作成・管理できるサービスです。手動でリソースを一つずつ作成する代わりに、テンプレートをアップロードするだけで必要なリソースをまとめて構築できます。なお、このハンズオンではCloudFormationテンプレートの内容を理解する必要はありません。テンプレートをそのままアップロードして環境を構築してください。

CloudFormationで作成されるリソースは以下のとおりです。

リソース 名前 説明
VPC cicd-handson-vpc 10.0.0.0/16のアドレス範囲を持つ仮想ネットワーク
パブリックサブネット cicd-handson-public-subnet ECSタスクを配置するサブネット
インターネットゲートウェイ cicd-handson-igw パブリックサブネットからのインターネットアクセス用
ルートテーブル cicd-handson-rt パブリックサブネット用のルーティング設定
セキュリティグループ cicd-handson-ecs-sg ECSタスク用(HTTP:8000を許可)
ECRリポジトリ test-repository コンテナイメージを格納するリポジトリ
ECSクラスター test-cluster ECSサービスを管理する論理グループ
タスク実行ロール ecsTaskExecutionRole-cicd-handson ECSタスクがCloudWatch Logsなどを操作するためのIAMロール
タスク定義 test-task ECSタスクの定義(イメージURI、CPU/メモリなど)
ECSサービス test-service タスク定義を元にECSタスクを実行・管理するサービス
CloudWatch Logs /ecs/test-task ECSタスクのログ出力先

4.1 テンプレートファイルの準備

まず、CloudFormationにアップロードするテンプレートファイルを用意します。以下のボタンからテンプレートファイルをダウンロードしてください。

ファイルがダウンロードできたら、次のステップに進みます。

4.2 CloudFormationスタックの作成

AWSマネジメントコンソールでCloudFormationのダッシュボードを開きます。検索バーに「CloudFormation」と入力し、サービスを選択してください。

CloudFormationのダッシュボードが表示されたら、スタックの作成 > 新しいリソースを使用標準)をクリックします。

ステップ1: テンプレートの指定

テンプレートの指定画面が表示されます。下記表に従い、設定を行ってください。

設定項目 設定の基準
テンプレートの準備 既存のテンプレートを選択 事前に作成したテンプレートファイルを使用するため
テンプレートソース テンプレートファイルのアップロード ローカルのテンプレートファイルを直接アップロードするため
テンプレートファイルのアップロード 先ほど保存したcicd-handson.yml ハンズオン環境を構築するテンプレートを指定するため

設定後、次へをクリックしてください。

ステップ2: スタックの詳細を指定

スタックの詳細を指定する画面が表示されます。下記表に従い、設定を行ってください。

設定項目 設定の基準
スタック名 cicd-handson スタックを識別するための名前

今回のテンプレートにはパラメータが定義されていないため、スタック名以外の入力はありません。設定後、次へをクリックしてください。

ステップ3: スタックオプションの設定

スタックオプションの設定画面が表示されます。基本的な設定はデフォルトのままで問題ありませんが、画面下部に「The following resource(s) require capabilities: [AWS::IAM::Role]」という警告が表示されます。これは、今回のテンプレートにIAMロール(ecsTaskExecutionRole-cicd-handson)が含まれているため、IAMリソースの作成を承認する必要があるというメッセージです。

AWS CloudFormation によって IAM リソースがカスタム名で作成される場合があることを承認します。」にチェックを入れてから、次へをクリックしてください。

ステップ4: 確認と作成

確認画面が表示されます。設定内容を確認し、送信をクリックします。

スタックの作成完了を確認

スタックの作成が開始されます。画面にはステータスがCREATE_IN_PROGRESSと表示され、リソースが順番に作成されていきます。完了まで数分かかります。

ステータスがCREATE_COMPLETEに変わったら、環境構築は完了です。画面を更新してもCREATE_COMPLETEにならない場合は、もう少し待ってから再度更新してください。

4.3 リソースの確認

CloudFormationでスタックの作成が完了したら、実際にどのようなリソースが作成されたのかを確認しておきましょう。ここで各リソースの役割を把握しておくことで、この後のワークフロー作成時に「なぜこのターゲットグループやECRを指定するのか」が理解しやすくなります。

CloudFormationでは、スタックから作成されたリソースをまとめて確認できるリソースタブが用意されています。AWSマネジメントコンソールを個別に開いて回る必要がないため、まずはこの画面で作成されたリソースを一覧で確認します。

CloudFormationのスタック一覧でcicd-handsonスタックをクリックし、上部のリソースタブを選択してください。スタックから作成されたリソースが一覧で表示されます。

以下のリソースがすべて作成されていること、ステータスがCREATE_COMPLETEになっていることを確認してください。

論理ID 物理ID リソースタイプ
VPC cicd-handson-vpc AWS::EC2::VPC
PublicSubnet cicd-handson-public-subnet AWS::EC2::Subnet
InternetGateway cicd-handson-igw AWS::EC2::InternetGateway
RouteTable cicd-handson-rt AWS::EC2::RouteTable
SecurityGroup cicd-handson-ecs-sg AWS::EC2::SecurityGroup
ECRRepository test-repository AWS::ECR::Repository
ECSCluster test-cluster AWS::ECS::Cluster
TaskExecutionRole ecsTaskExecutionRole-cicd-handson AWS::IAM::Role
TaskDefinition test-task AWS::ECS::TaskDefinition
ECSService test-service AWS::ECS::Service
LogGroup /ecs/test-task AWS::Logs::LogGroup

各リソースの物理IDをクリックすると、そのリソースの管理画面(VPCコンソールやECSコンソールなど)に直接遷移できます。気になるリソースがあれば、実際に遷移して中身を確認してみてください。

リソース一覧にすべてのリソースが表示され、全てCREATE_COMPLETEになっていれば、環境構築は正常に完了しています。

5. OIDC接続

AWSリソースの準備ができたので、次はGitHub ActionsからAWSへ接続するための設定を行います。ビルドしたコンテナイメージをECRにプッシュしたり、ECSにデプロイしたりするには、GitHub ActionsからAWSリソースへアクセスする必要があります。そのための前準備としてGitHub ActionsとAWSの接続設定を行います。

5.1 なぜOIDCを使うのか

GitHub ActionsからAWSに接続する方法はいくつかありますが、大きく分けると次の2通りになります。

  1. IAMユーザのアクセスキーID・シークレットアクセスキーをGitHubのSecretsに登録し、ワークフローから使用する
  2. OIDCで、GitHub Actionsが発行したIDトークンをAWSが検証し、一時的なクレデンシャルを発行してアクセスする

1の方法はシンプルですが、アクセスキーが万が一漏洩すると第三者がAWSリソースに恒久的にアクセスできてしまうというリスクがあります。また、定期的なローテーションも必要になります。

一方、2のOIDC(OpenID Connect)を使う方法では、アクセスキーを保存する必要がなく、毎回一時的な短命クレデンシャルが発行されます。漏洩リスクが大幅に軽減され、ローテーションも不要になるため、本番環境ではこちらが推奨される方法です。今回は実務で使える形を体験するために、OIDCでの接続を採用します。

📝 OIDCとは
OIDC(OpenID Connect)は、認証を行うための仕組みの一つです。GitHub ActionsとAWSの場合、ワークフロー実行時にGitHubが「このリポジトリ・ブランチから実行された」というIDトークンを発行し、AWSがそれを検証して一時的なクレデンシャルを発行します。信頼関係を事前にAWS側に登録しておくことで、「特定のリポジトリからのワークフロー実行時だけAWSへのアクセスを許可する」といった細かい制御ができます。

5.2 OIDCの設定に必要な作業

OIDCでGitHub ActionsとAWSを接続するには、AWS側で2つの設定を行う必要があります。

作業 内容
IAM Identity Providerの作成 GitHubのOIDCプロバイダ(token.actions.githubusercontent.com)をAWSに登録する
IAMロールの作成 GitHub Actionsが引き受けるロールを作成し、信頼関係と権限を設定する

これらを順番に作成していきます。

5.3 IAM Identity Providerの作成

まず、GitHubをOIDCプロバイダとしてAWSに登録します。これにより、GitHubから発行されたIDトークンをAWSが信頼できるようになります。

AWSマネジメントコンソールでIAMのダッシュボードを開きます。検索バーに「IAM」と入力し、サービスを選択してください。

左メニューからIDプロバイダを選択し、プロバイダを追加をクリックします。

以下の設定でプロバイダを追加します。

設定項目 設定の基準
プロバイダのタイプ OpenID Connect OIDCでGitHubと接続するため
プロバイダのURL https://token.actions.githubusercontent.com GitHub ActionsのOIDCエンドポイント
対象者 sts.amazonaws.com AWS STSをターゲットとするため

入力後、プロバイダを追加をクリックします。

IDプロバイダの一覧にtoken.actions.githubusercontent.comが追加されていれば、ここまでの操作は完了です。

💡 ポイント
以前はOIDCプロバイダを作成する際に「サムプリント(指紋)」の登録が必要でしたが、現在のAWSでは自動的に検証される仕組みになっているため、この手順は不要です。プロバイダのURLと対象者を指定するだけで登録できます。

5.4 IAMロールの作成

次に、GitHub Actionsが引き受けるIAMロールを作成します。このロールには、「どのGitHubリポジトリからのアクセスを許可するか」という信頼関係と、「そのロールがAWSで何をできるか」という権限の2つを設定します。

IAMのダッシュボードの左メニューからロールを選択し、ロールを作成をクリックします。

信頼されたエンティティの選択

ロール作成画面で、信頼されたエンティティのタイプを選択します。

設定項目 設定の基準
信頼されたエンティティタイプ ウェブアイデンティティ OIDCプロバイダを信頼するため
アイデンティティプロバイダー token.actions.githubusercontent.com 先ほど作成したOIDCプロバイダ
Audience sts.amazonaws.com AWS STSをターゲットとするため
GitHub組織 ご自身のGitHubユーザ名 リポジトリのオーナー名
GitHubリポジトリ devopscamp-cicd-handson 対象のリポジトリ名
GitHubブランチ main mainブランチからのアクセスだけを許可するため。feature/xxxブランチ等からAWSを操作できないようにする
💡 ポイント
GitHub組織には、ご自身のGitHubユーザ名(個人アカウントの場合)または組織名を指定します。GitHubリポジトリには、このハンズオンで作成したリポジトリ名(devopscamp-cicd-handson)を指定してください。リポジトリ名を変えている場合は、その名前を指定します。
💡 ポイント
ブランチを空欄にすると、リポジトリ内のあらゆるブランチからAWSへアクセスできてしまいます。今回のワークフローは mainへのpush でのみ動作する前提なので、mainブランチのみを信頼する設定にしておくとセキュリティが高まります。feature/xxxブランチ等から意図せずAWSリソースが操作される事故を防げます。

将来、ステージング環境を追加する場合は staging ブランチを許可リストに追加する、という形で拡張できます。

設定後、次へをクリックします。

許可ポリシーの追加

このロールには、あとのステップでインラインポリシー(特定のリソース・アクションに絞ったカスタムポリシー)として必要な権限を追加します。このステップではAWS管理ポリシーは何も選択せず、そのまま次へをクリックしてください。

💡 ポイント
AWSが用意する管理ポリシー(例: AmazonEC2ContainerRegistryPowerUser)は広く使いやすい反面、リソースやアクションが広めに許可されがちです。本ハンズオンでは「最小権限の原則」を体感してもらうため、必要なECR・ECSのアクションだけを、対象リソースも絞ったインラインポリシーで付与します。

名前、確認、および作成

最後にロールの名前と説明を入力します。

設定項目 設定の基準
ロール名 github-actions-deploy-role GitHub Actionsからのデプロイに使うロール
説明 Role for deployment from GitHub Actions ロールの目的を明確にするため

入力後、画面下部のロールを作成をクリックします。

⚠️ 「ロールの説明が無効です」エラーが出る場合
AWSのIAMロール説明欄は日本語を受け付けません。英数字と記号のみで入力してください。使用可能な文字は A-Za-z0-9、タブ、改行、および `_+=,.@-/[{}]!#$%^*():;"'`` の記号です。説明欄はオプションなので、空欄のままでも構いません。

ロール一覧にgithub-actions-deploy-roleが表示されていれば、ここまでの操作は完了です。

デプロイ権限の追加(インラインポリシー)

作成したロールにはまだ権限が何もついていません。ECRへのイメージプッシュとECSへのデプロイを行うために、インラインポリシーとして必要な権限を設定します。

先ほど作成したgithub-actions-deploy-roleの詳細画面を開きます。

許可タブから許可を追加 > インラインポリシーを作成をクリックします。

JSONタブを選択し、以下の内容を入力します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECRAuth",
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Sid": "ECRPush",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage",
        "ecr:BatchGetImage"
      ],
      "Resource": "arn:aws:ecr:*:*:repository/test-repository"
    },
    {
      "Sid": "ECSDeploy",
      "Effect": "Allow",
      "Action": [
        "ecs:UpdateService",
        "ecs:DescribeServices",
        "ecs:DescribeTaskDefinition",
        "ecs:RegisterTaskDefinition",
        "ecs:DeregisterTaskDefinition",
        "ecs:ListTasks",
        "ecs:DescribeTasks"
      ],
      "Resource": "*"
    },
    {
      "Sid": "PassExecutionRole",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::*:role/ecsTaskExecutionRole-cicd-handson"
    }
  ]
}

ポリシーは4ブロックに分かれています。各ブロックが「何に」「何の権限を」「なぜ」与えているかを以下の表で解説します。

Sid 対象リソース 与える権限 なぜ必要か
ECRAuth 全ECR(* ecr:GetAuthorizationToken docker login 時にECRから一時認証トークンを取得する必要がある。このAPIはアカウント単位の操作のため、リポジトリでリソースを絞れない仕様
ECRPush test-repository リポジトリのみ ecr:BatchCheckLayerAvailability / ecr:InitiateLayerUpload / ecr:UploadLayerPart / ecr:CompleteLayerUpload / ecr:PutImage / ecr:BatchGetImage docker push でイメージをアップロードするために必要な一連の操作。レイヤーの確認・アップロード開始・転送・完了・登録までをカバーする。リソースを絞ることで他のECRリポジトリには手出しできないようにしている
ECSDeploy 全ECS(* ecs:UpdateService / ecs:DescribeServices / ecs:DescribeTaskDefinition / ecs:RegisterTaskDefinition / ecs:DeregisterTaskDefinition / ecs:ListTasks / ecs:DescribeTasks タスク定義を新しいリビジョンとして登録し、ECSサービスを更新してデプロイするために必要。RegisterTaskDefinition はリソース絞り込みが利かない仕様のため、本ハンズオンでは * としている
PassExecutionRole ecsTaskExecutionRole-cicd-handson ロールのみ iam:PassRole ECSがタスクを起動する際、コンテナにCloudWatch Logsへの書き込み権限などを与える「タスク実行ロール」をECS側に渡す必要がある。CloudFormationで作成した特定ロールだけに絞ることで、任意のIAMロールを勝手に渡せないようにしている

次へをクリックし、ポリシー名にDeployPolicyと入力してポリシーの作成をクリックします。

github-actions-deploy-roleの許可一覧にDeployPolicyが追加されていれば、ここまでの操作は完了です。

5.5 ロールARNの確認

作成したIAMロールのARNは、後ほどGitHub Actionsのワークフローから参照します。github-actions-deploy-roleの詳細画面の上部に表示されているARNを確認しておきましょう。

ARNは以下のような形式になっています。

arn:aws:iam::123456789012:role/github-actions-deploy-role

この中の123456789012の部分がAWSアカウントIDです。このアカウントIDはGitHub Actionsワークフローから参照するため、次のステップでGitHubのシークレットに登録します。

5.6 GitHubシークレットの登録

GitHub Actionsのワークフローからロールを参照する際、AWSアカウントIDをコードにハードコードすると、リポジトリを見た人にアカウントIDが知られてしまいます。アカウントID自体は機密情報ではありませんが、安全な運用としてはGitHubのシークレット機能に登録し、ワークフローから動的に参照する形が推奨されます。

ここでは、AWSアカウントIDをGitHubシークレットに登録します。

GitHubの対象リポジトリ(devopscamp-cicd-handson)のページを開きます。

Settingsタブをクリックし、左メニューからSecrets and variables > Actionsを選択します。

New repository secretボタンをクリックし、以下の内容でシークレットを登録します。

設定項目 設定の基準
Name AWS_ACCOUNT_ID ワークフローから参照する際の名前
Secret 先ほど確認したAWSアカウントID(例: 123456789012 対象のAWSアカウント

Add secretボタンをクリックすると、シークレットが登録されます。

シークレット一覧にAWS_ACCOUNT_IDが表示されていれば、ここまでの操作は完了です。

6. 自動ビルドとデプロイ

OIDC接続の準備が整ったので、いよいよGitHub Actionsを使った自動ビルドと自動デプロイのワークフローを構築していきます。

手動でデプロイを行う場合、Pull Requestをマージしたあとに「コンテナイメージの作成」「ECRへのプッシュ」「ECSへのデプロイ」といった作業を開発者が実行する必要があります。これらの一連の作業を自動化することで、mainブランチにマージしただけで本番環境に反映されるという、真のCI/CDパイプラインが完成します。

自動ビルドと自動デプロイは別のジョブとして定義することもできますが、今回は同一のジョブで実行する流れとします。別々のジョブにすると、ジョブ間でビルドしたイメージの情報を受け渡す必要があり、複雑になるためです。

6.1 作業用ブランチの作成

ワークフローファイルを追加する前に、作業用のブランチを作成します。ブランチを分けずにmainブランチに直接コミットしてしまうと、「Pull Requestを作成してマージする」という実務的な流れを体験できません。CI/CDパイプラインは「レビュー後のマージ」をトリガーにデプロイする前提で組むため、ここでしっかりと作業用ブランチを分けておきましょう。

Visual Studio Codeでdevopscamp-cicd-handsonフォルダを開いたうえで、ターミナルを開きます。以下のgit switchコマンドで、feature/add-cicd-pipelineというブランチを作ります。-cオプションは新しいブランチを作成して切り替えるという意味です。

git switch -c feature/add-cicd-pipeline

Switched to a new branch 'feature/add-cicd-pipeline'と表示されていれば、ここまでの操作は完了です。

6.2 自動ビルド&デプロイの流れ

自動ビルドとデプロイの流れを説明します。FastAPIで作成したAPIをコンテナ化し、Amazon ECSにデプロイするまでの流れを自動化しています。

トリガーはmainブランチへのPushです。これは、直接Pushした場合だけでなく、Pull RequestのマージによるPushにも対応しています。

処理の流れは以下のとおりです。

  1. GitHub Actionsでソースコードをチェックアウトする
  2. 先ほど作成したOIDCの仕組みを使って、AWSへの認証を行う
  3. Amazon ECRへログインする
  4. ソースコードからコンテナイメージを作成し、ECRにPushする(自動ビルド)
  5. タスク定義ファイルのプレースホルダをシークレット値で置換する
  6. 置換済みタスク定義を元に、Amazon ECSへデプロイする(自動デプロイ)

これらの一連の流れを、GitHub Actionsのワークフローとして設定します。

6.3 ワークフローの作成

まず、ワークフローファイルを作成して全体の流れを定義します。

ファイルの作成

ビルドとデプロイの処理は、mainブランチへのマージ(Push)時に動作するワークフローとして構築します。GitHub Actionsは.github/workflowsフォルダ配下のYAMLファイルをワークフローとして自動的に認識するため、このルールに沿ってファイルを配置します。今回はdeploy.ymlというファイル名で作成します。

devopscamp-cicd-handson/
└── .github/
    └── workflows/
        └── deploy.yml  ← このファイルを作成

作成したファイルに以下の内容を記述して保存します。

name: Build and Deploy to ECS

on:
  push:
    branches:
      - main  # mainブランチへのpushをトリガーに実行

permissions:
  id-token: write  # OIDCトークン発行に必要
  contents: read   # リポジトリ内容を取得するために必要

env:
  AWS_REGION: ap-northeast-1
  ECR_REPOSITORY: test-repository   # ECRリポジトリ名
  ECS_CLUSTER: test-cluster         # ECSクラスター名
  ECS_SERVICE: test-service         # ECSサービス名

jobs:
  build-and-deploy:
    name: Build and Deploy
    runs-on: ubuntu-latest

    steps:
      # ソースコードをチェックアウト
      - name: Checkout source
        uses: actions/checkout@v5

      # AWS認証情報を設定 (OIDCを利用)
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v5
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-deploy-role
          aws-region: ${{ env.AWS_REGION }}

      # ECRへログイン
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      # Dockerイメージをビルド&プッシュ
      - name: Build and Push Docker image
        run: |
          IMAGE_REPO=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}
          docker build -t $IMAGE_REPO:latest .
          docker push $IMAGE_REPO:latest

      # タスク定義のプレースホルダをシークレットの値で置換
      - name: Render task definition
        run: |
          sed -i "s|<AWS_ACCOUNT_ID>|${{ secrets.AWS_ACCOUNT_ID }}|g" ecs-taskdef.json

      # ECSへデプロイ
      - name: Deploy to ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ecs-taskdef.json
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

6.4 ワークフローの説明

ワークフローの説明を行います。

名称

name: Build and Deploy to ECS

ワークフローの名称として、Build and Deploy to ECSという名前をつけています。

トリガー

on:
  push:
    branches:
      - main

トリガーとして、mainブランチに対してコードが変更された時に起動するように設定しています。

permissions:
  id-token: write  # OIDCトークン発行に必要
  contents: read   # リポジトリ内容を取得するために必要

Permissionsにより、GitHubが操作を行う上での権限を設定します。

id-token: writeは、GitHub Actions が OIDCトークンを発行するための権限です。今回はOIDCトークンを利用してAWSへアクセスするため、この権限が必要になります。

contents: readは、リポジトリのコードを読み取るための権限です。actions/checkoutがソースを取得するのに必要となります。

ジョブ

jobs:
  build-and-deploy:
    name: Build and Deploy
    runs-on: ubuntu-latest

ジョブとして、build-and-deployというジョブを定義しています。名前にBuild and Deploy、ランナーとしてubuntu-latestを指定しているため、ubuntuの最新のランナー環境で動作します。

ステップ1:コードをチェックアウト

- name: Checkout source
  uses: actions/checkout@v5

mainブランチへのpushをトリガーとして実行されるため、このステップではmainブランチの最新のコードがチェックアウトされます。

actions/checkout

ステップ2:AWS認証情報を設定

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v5
  with:
    role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-deploy-role
    aws-region: ${{ env.AWS_REGION }}

aws-actions/configure-aws-credentialsは、AWSにログインするための認証情報を設定するアクションです。role-to-assumeにはAWSに接続するときに使用するIAMロールを、aws-regionには操作対象のリージョンを指定します。

aws-actions/configure-aws-credentials

ステップ3:ECRへのログイン

- name: Login to Amazon ECR
  id: login-ecr
  uses: aws-actions/amazon-ecr-login@v2

aws-actions/amazon-ecr-loginで ECR へのログインを行います。このアクションを使うためには、事前にステップ2でAWSの認証情報を設定しておく必要があります。

aws-actions/amazon-ecr-login

ECRへの認証は、内部的には認可トークンを取得して docker login に渡す仕組みになっています。Private registry authentication in Amazon ECR(AWS公式ドキュメント)の原文(英語表示時)では以下のように述べられています。

An authentication token is used to access any Amazon ECR registry that your IAM principal has access to and is valid for 12 hours.

「認可トークンはIAMプリンシパルがアクセスできる任意のECRレジストリに対して有効で、12時間で失効する」と読み取れます。aws-actions/amazon-ecr-login はこのトークン取得と docker login を裏側でまとめて実行してくれるアクションです。なお、AWSドキュメントは画面右上の言語セレクタから日本語表示に切り替えられます。

ステップ4:Dockerイメージをビルド&プッシュ

- name: Build and Push Docker image
  run: |
    IMAGE_REPO=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}
    docker build -t $IMAGE_REPO:latest .
    docker push $IMAGE_REPO:latest

Dockerコマンドを使って、イメージのビルドとプッシュを行います。docker build -t $IMAGE_REPO:latest .は、カレントディレクトリにあるDockerfileを使ってDockerイメージを作成するコマンドです。今回はFastAPIのコードがリポジトリのルート直下に配置されているため、ビルドコンテキストとして.(カレントディレクトリ)を指定しています。

docker pushは、作成したイメージをECRのリポジトリにアップロードするコマンドです。

ステップ5:タスク定義のプレースホルダを置換

- name: Render task definition
  run: |
    sed -i "s|<AWS_ACCOUNT_ID>|${{ secrets.AWS_ACCOUNT_ID }}|g" ecs-taskdef.json

ecs-taskdef.jsonに書かれた<AWS_ACCOUNT_ID>というプレースホルダを、GitHubシークレットに登録したAWSアカウントIDの値で置換します。これによりリポジトリには常にプレースホルダだけが残り、アカウントIDが直接コミットされない状態を保てます。

sed -iはファイルを直接書き換えるオプションで、s|置換前|置換後|gという構文で全出現箇所を一括置換します。

ステップ6:ECSにデプロイ

- name: Deploy to ECS
  uses: aws-actions/amazon-ecs-deploy-task-definition@v2
  with:
    task-definition: ecs-taskdef.json
    service: ${{ env.ECS_SERVICE }}
    cluster: ${{ env.ECS_CLUSTER }}
    wait-for-service-stability: true

aws-actions/amazon-ecs-deploy-task-definitionで ECS へのデプロイを行います。task-definitionには直前のステップで置換済みのecs-taskdef.jsonを、serviceclusterには対象のECSサービス名とクラスタ名を指定します。

wait-for-service-stability: trueを指定すると、デプロイ後に新しいタスクが正常に稼働し、サービスが安定状態になるまで待機します。これを指定しないと、タスクが起動しきっていない状態でワークフローが終了してしまい、不具合に気づきにくくなる可能性があります。

6.5 タスク定義ファイルの作成

ワークフローのステップ6でtask-definition: ecs-taskdef.jsonを参照していました。このファイルは、ECSに対して「どのようなコンテナを・どのようなリソース設定で動かすのか」を伝えるためのタスク定義ファイルです。ワークフロー実行時にこのファイルを読み込み、新しいタスク定義をECSに登録してサービスを更新します。

リポジトリのルート(devopscamp-cicd-handson/直下)にecs-taskdef.jsonというファイルを作成します。

devopscamp-cicd-handson/
├── ecs-taskdef.json  ← このファイルを作成
├── Dockerfile
├── main.py
└── ...

作成したファイルに以下の内容をそのまま記述して保存します。<AWS_ACCOUNT_ID>はプレースホルダで、ワークフロー実行時にGitHubシークレットの値で自動置換するため、ここではそのままで構いません。

{
  "family": "test-task",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/ecsTaskExecutionRole-cicd-handson",
  "containerDefinitions": [
    {
      "name": "app",
      "image": "<AWS_ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/test-repository:latest",
      "portMappings": [
        {
          "containerPort": 8000,
          "protocol": "tcp"
        }
      ],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/test-task",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]
}

コードを解説します。

項目 説明
family タスク定義の名前。CloudFormationで作成したタスク定義と同じtest-taskを指定
networkMode Fargateではawsvpcを指定する必要がある
requiresCompatibilities FARGATEを指定してFargate互換のタスク定義にする
cpu, memory タスクのCPU・メモリサイズ
executionRoleArn ECSタスクがCloudWatch Logsへの書き込みなどを行うために必要なロール
containerDefinitions.image ECRにPushされたイメージのURI
containerDefinitions.portMappings コンテナが公開するポート(FastAPIはデフォルトで8000)
containerDefinitions.logConfiguration ログをCloudWatch Logsに送る設定

networkMode: awsvpc がFargateで必須である点は、Amazon ECS task definition parameters for Fargate(AWS公式ドキュメント)に明記されています。原文(英語表示時)では以下のように述べられています。

For Amazon ECS tasks hosted on Fargate, the awsvpc network mode is required.

「Fargate上のECSタスクでは awsvpc ネットワークモードが必須である」と読み取れます。各パラメータの取りうる値や cpu / memory の組み合わせ表も同ページにまとまっています。なお、AWSドキュメントは画面右上の言語セレクタから日本語表示に切り替えられます。

💡 ポイント
image:latestを指定していますが、GitHub Actionsのワークフロー実行時に、この:latestタグのイメージがECRに最新のイメージとしてPushされた状態で、ECSに新しいタスク定義として登録されます。つまり、デプロイのたびに常に最新のイメージが使われる仕組みです。
💡 ポイント
アカウントIDをこのJSONに直接書いてしまうと、GitHubシークレットに登録した意味がなくなります(リポジトリにそのまま残るため)。そこで本ハンズオンでは<AWS_ACCOUNT_ID>というプレースホルダで書いておき、ワークフロー実行時にシークレットの値で置換してから使用します。置換は次のセクションで追加するsedコマンドのステップで行います。

AWSアカウントIDは厳密には機密情報ではありません(AWS公式も「秘密ではない」と明言)。ただし一度シークレットで扱うと決めたなら、成果物にもハードコードしないのが一貫性ある設計です。この考え方は、今後他の準機密情報(リージョン依存のリソースIDなど)を扱う際にも応用できます。

6.6 実行してみる

ワークフローの作成が完了しましたので、実際にデプロイが動くかを確認します。今回のワークフローはmainブランチへのPushで起動するため、作業ブランチ(feature/add-cicd-pipeline)の内容をPull Requestでマージすれば、mainブランチへの変更が発生してデプロイが自動実行されます。

GitHubにPushする

まず、作業ブランチで行った変更(ecs-taskdef.jsondeploy.ymlの追加)をGitHubにPushします。Visual Studio Codeのターミナルで、以下のコマンドを1つずつ実行します。

ステージングします。

git add .

コミットします。

git commit -m "add deploy workflow"

Pushします。

git push origin feature/add-cicd-pipeline

GitHubのリポジトリページを開いて、Pushしたファイルが反映されていることを確認します。

Pull Requestの作成

Pushが完了したので、feature/add-cicd-pipelineブランチからmainブランチに対してPull Requestを作成します。ビルドとデプロイのワークフローはmainブランチへのマージをトリガーとして実行されるため、まずPull Requestを作成し、そこからマージする流れを取ります。

GitHubのリポジトリページで、Pull requestsタブをクリックします。

New pull requestのボタンをクリックします。

ブランチとして、左側のbasemain、右側のcomparefeature/add-cicd-pipelineを選択した上で、Create pull requestをクリックします。

タイトルと説明欄はそのままで問題ありません。さらにCreate pull requestをクリックすると、Pull Requestが作成されます。

Pull Requestのマージ

Pull Requestが作成できたら、そのままマージします。ビルドとデプロイのワークフローはマージをトリガーとして実行されるため、ここでマージすることで初めてデプロイが動き出します。

Pull Requestの画面で、Merge pull requestのボタンをクリックします。

続いて、確認画面でConfirm mergeボタンをクリックすると、マージが行われます。

マージが完了すると、mainブランチに対してPushが発生したことになり、Build and Deploy to ECSワークフローが自動的に起動します。GitHubのActionsタブを開くと、ワークフローの実行状況を確認できます。

しばらくするとデプロイのワークフローが正常終了します。

6.7 リソースの確認

ワークフローが正常終了したので、実際にAWS側にデプロイの結果が反映されているかを確認します。ワークフローが「成功」と表示されていても、何らかの理由で想定どおりにリソースが更新されていない可能性もあるため、AWSコンソールで直接確認しておくことが重要です。

まずは、ECRのリポジトリに、新しいイメージが作成されていることを確認します。イメージタグがlatestとなっているイメージが作成されていて、プッシュされた日時が、先ほどマージしたタイミングと同じであれば問題ありません。

続いて、ECSへのデプロイが行われていることを確認します。

ECSのクラスターからtest-serviceを選択し、デプロイタブを開くと、新しいリビジョンでのデプロイが完了していることが確認できます。

6.8 タスクの起動(DesiredCountの変更)

ECSサービスにはデプロイは反映されましたが、実はまだタスクは起動していません。CloudFormationで作成した時点でDesiredCount(実行するタスク数)を0に設定していたためです。

DesiredCountを最初から1にしなかった理由は、初回のCloudFormationスタック作成時にはECRリポジトリにイメージがまだ存在しないため、ECSがタスクを起動しようとしても失敗を繰り返してしまうためです。CI/CDパイプラインで初回のデプロイが完了し、ECRにイメージがPushされた今のタイミングで、手動でタスク数を1に変更します。

ECSのクラスターtest-clusterサービスタブで、test-serviceを選択し、更新ボタンをクリックします。

サービス更新画面で、以下の設定を行います。

設定項目 設定の基準
必要なタスク 1 タスクを1つ起動させるため

その他の項目はデフォルトのままで、更新をクリックします。

数十秒〜1分程度で、新しいタスクが起動します。ECSのクラスターtest-clusterタスクタブを開くと、タスクが1件起動していれば、ここまでの操作は完了です。

6.9 動作確認

ECS側でタスクが起動したことを確認できたら、実際にAPIを呼び出して「本当に正常に動作しているか」を確かめます。デプロイが完了してもアプリケーション側のバグや設定ミスで動作していないケースもあるため、最後にAPIを叩いて期待通りのレスポンスが返ってくることを確認するのがベストプラクティスです。

今回はECSタスクをパブリックサブネットに配置しているため、ECSタスクのパブリックIPアドレスを使ってAPIを呼び出します。

まずは、タスクの詳細を開きます。

その後、ネットワーキングのタブを開き、パブリックIPをコピーします。

Windowsであればコマンドプロンプト、Macであればターミナルを開き、CURLコマンドでAPIのヘルスチェックエンドポイントを呼び出してみます。

curl http://<パブリックIP>:8000/health

以下のようにヘルスチェックのレスポンスが返ってくれば、コンテナが起動していることが確認できます。

{"status":"ok"}

続いて、動作確認用のエンドポイントも呼び出してみます。

curl http://<パブリックIP>:8000/

以下のようにメッセージのレスポンスが返ってくれば、アプリケーションが正常に動作しています。

{"message":"API呼び出しテストに成功しました"}

{"status":"ok"}{"message":"API呼び出しテストに成功しました"}の両方が表示されていれば、ここまでの操作は完了です。

6.10 更新してみる

初回デプロイは成功しましたが、CI/CDパイプラインの本当の価値は「繰り返し使えること」にあります。コードを変更するたびに自動でビルド・デプロイされ、常に最新のコードが本番環境に反映されることを体験するために、APIのレスポンスメッセージを少し変更してから再度デプロイしてみます。

コードの変更

main.pyのファイルを開き、root関数のレスポンスメッセージに(ver2)を追加します。

@app.get("/")
def root():
    return {"message": "API呼び出しテストに成功しました(ver2)"}

その後、変更をGitコマンドでGitHubにプッシュします。

まず、前回の作業ブランチ(feature/add-cicd-pipeline)からmainブランチに切り替え、最新のmainの状態をローカルに取り込みます。

git switch main
git pull origin main

続いて、今回の変更用に新しいブランチを作成します。

git switch -c feature/update-message

変更をステージングしてコミットします。

git add .
git commit -m "update message to ver2"

Pushします。

git push origin feature/update-message

マージ

続いて、GitHubの画面にてPull Requestを作成し、マージを行います。マージを行うと、GitHub Actionsのワークフローが正常終了することを確認します。

動作確認

CURLコマンドを実行して、変更の反映を確認します。なお、デプロイすることでECSタスクが新しくなるため、パブリックIPアドレスは変更されているのでご注意ください。

curl http://<パブリックIP>:8000/

実行結果のメッセージに(ver2)が含まれていればOKです。

{"message":"API呼び出しテストに成功しました(ver2)"}

レスポンスに(ver2)が含まれていれば、コードの変更が自動デプロイを通じて正しく反映されていることが確認できます。コードを書き換えてPushするだけで本番環境が更新されることが確認できました。これがCI/CDパイプラインの最大のメリットです。

7. リソースの削除

最後に、AWSのリソースを削除します。AWSのリソースは起動している間ずっと料金が発生し続けているため、必ず削除しておきましょう。

7.1 ECRリポジトリのイメージ削除

CloudFormationでスタックを削除する際、ECRリポジトリにイメージファイルが存在すると削除に失敗します。そのため、先にECRリポジトリ内のイメージを削除しておく必要があります。

AWSマネジメントコンソールでECRのダッシュボードを開きます。検索バーに「ECR」と入力し、「Elastic Container Registry」を選択してください。

リポジトリ一覧からtest-repositoryをクリックし、表示されているイメージをすべて選択します。選択後、削除をクリックしてください。

確認画面が表示されるため、確認文字列を入力し、削除をクリックします。

イメージの一覧が空になっていれば、ここまでの操作は成功です。

7.2 CloudFormationスタックの削除

AWSマネジメントコンソールでCloudFormationのダッシュボードを開きます。

スタック一覧からcicd-handsonを選択し、削除をクリックします。

確認画面が表示されるので、削除をクリックします。

スタックの削除が開始され、ステータスがDELETE_IN_PROGRESSに変わります。削除完了まで数分かかります。

ステータスがDELETE_COMPLETEになれば、CloudFormationで作成したリソースがすべて削除されています。スタック一覧からcicd-handsonが消えていることを確認してください。

7.3 IAMリソースの削除

このハンズオンで手動作成したIAMリソースは、CloudFormationスタックには含まれていないため、別途削除する必要があります。

AWSマネジメントコンソールでIAMのダッシュボードを開きます。

まず、左メニューからロールを選択し、github-actions-deploy-roleを検索します。該当のロールを選択し、削除をクリックしてください。確認画面でロール名を入力し、削除を実行します。

次に、左メニューからIDプロバイダを選択し、token.actions.githubusercontent.comを選択します。削除をクリックし、確認画面で削除を実行してください。

IAMのロール一覧とIDプロバイダ一覧から、それぞれのリソースが消えていれば、ここまでの操作は完了です。

8. まとめ

このハンズオンでは、GitHub ActionsとAWS ECS/Fargateを使ったFastAPIアプリケーションの自動ビルドと自動デプロイを体験しました。

  • OIDCを利用してGitHub ActionsからAWSに安全に接続できる
  • 自動ビルドでDockerイメージを作成しECRにプッシュできる
  • 自動デプロイでECS Fargateにコンテナをデプロイできる
  • Pull Requestのマージをトリガーに、デプロイまで自動実行できる
  • ECSのデプロイ機能により、ダウンタイムなしで新バージョンへ切り替えられる