LoginSignup
10
8

More than 1 year has passed since last update.

CodePipeline + Fargate + FastAPIで本番・開発・ローカル毎に環境変数が変わるCI/CDを構築してみた

Posted at

はじめに

初めまして。現在、新卒1年目でグーホームチームに所属しているプロトソリューションの川崎です。
今回は業務でAPIサーバーのCI/CDを構築する業務があり、その中で得た知見を書いていこうと思います。

環境

【筆者ローカル環境】
・Windows
・WSL(コントリビューション:Ubuntu-20.04)
・Docker 20.10.23

【使用した主なAWSサービス】
・CodeCommit
・CodeBuild
・CodePipeline
・Elastic Container Registry
・Elastic Container Service(ECS on Fargate)

この記事で分かること

CodePipelineを使用した、ECS on Fargateにおける開発環境・本番環境における各環境変数の設定方法が分かります。
今回紹介する方法以外にも環境変数の設定の仕方は様々あるかと思いますが、個人的にはフォルダ直下に環境変数がある方が見やすいので今回はそのような構成をお伝えします。
また、CI/CDを主軸にしているので、基本的なGitの使用・運用方法、Docker、VPC、IAM、FastAPIの説明は省略しているのでご容赦ください。

手順や本論

フォルダ構成とファイルは以下になります。
appspec.ymlとtaskdef.jsonは途中で書くため、最初には記載しません。

フォルダ構成
tech_press/
 ├ backend/
 │ ├ app/
 │ │   ├ main.py
 │ │  └ requirements.txt
 │ └ Dockerfile
 ├ pipeline/
 │ ├ dev/
 │ │   ├ appspec.yml
 │ │  └ taskdef.json
 │ └ product/
 │     ├ appspec.yml
 │    └ taskdef.json
 ├ .env.dev
 ├ .env.local
 ├ .env.product
 ├ buildspec.yml
 └ docker-compose.yaml

tech_press/backend/app/main.py
import os
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

@app.get("/check")
async def root():
    container_env = os.environ['CONTAINER_ENV']
    return {"Check": container_env + " is healthy"}
tech_press/backend/app/requirements.txt
fastapi==0.75.0
uvicorn==0.16.0
tech_press/backend/Dockerfile
FROM tiangolo/uvicorn-gunicorn:python3.7

COPY ./app /app

RUN pip install -U pip &&\
    pip install --no-cache-dir -r /app/requirements.txt

ARG CONTAINER_ENV

ENV CONTAINER_ENV=${CONTAINER_ENV}
tech_press/.env.dev
# COMMON
TIMEZONE=Asia/Tokyo

# API
API_HOST=0.0.0.0
API_PORT=8080

CONTAINER_ENV=Devel
tech_press/.env.local
# COMMON
TIMEZONE=Asia/Tokyo

# API
API_HOST=0.0.0.0
API_PORT=8080

CONTAINER_ENV=Local
tech_press/.env.product
# COMMON
TIMEZONE=Asia/Tokyo

# API
API_HOST=0.0.0.0
API_PORT=8080

CONTAINER_ENV=Product
tech_press/docker-compose.yaml
version: '3.8'
services:
  fastapi:
    container_name: tech-press-api
    image: tech-press-api
    build:
      context: ./backend
      dockerfile: Dockerfile
      args:
        - CONTAINER_ENV
    volumes:
      - ./backend/app:/app
    environment:
      - TZ=${TIMEZONE}
    command: uvicorn main:app --reload --host ${API_HOST} --port ${API_PORT}
    ports:
      - ${API_PORT}:${API_PORT}

1.まずはローカルで実行

まずはローカル環境下でFastAPIを立ち上げます。

(初回実行時)

docker compose --env-file .env.local up -d --build

(2回目以降)

docker compose --env-file .env.local up -d 

下記画像のように
http://localhost:8080/check
「{"Check":"Local is healthy"}」のJSONレスポンスが返ってきたらOKです
コメント 2023-01-24 132745.png

2.CodeCommit リポジトリ作成

CodeCommit -> リポジトリを作成 でリポジトリを作成します
今回は「tech-press」というリポジトリを作成しました
コメント 2023-01-24 135410.png

3.開発環境パイプラインを構築

ここからは、まず開発環境を構築していきます。

3-1.開発ECRのリポジトリ作成

Elastic Container Registry -> 「リポジトリを作成」でリポジトリを作成します。
コメント 2023-01-25 100743.png

他の設定はデフォルトのまま、「リポジトリを作成」を押下します。

次にCodeBuildからECRリポジトリにアクセス許可を与える設定をする必要があります。
Elastic Container Registry -> 作成したリポジトリ(今回でいうと「dev-tech-press-api」)の横にあるラジオボタンを選択 -> アクション -> 許可 -> ポリシーJSONの編集 を選択し、以下を追加します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "CodeBuildAccess",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<AWSアカウントID>:root",
        "Service": "codebuild.amazonaws.com"
      },
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer"
      ],
      "Condition": {
        "StringEquals": {
          "aws:SourceArn": "arn:aws:codebuild:ap-northeast-1:<AWSアカウントID>:project/dev-tech-press",
          "aws:SourceAccount": "<AWSアカウントID>"
        }
      }
    }
  ]
}

Conditionのaws:SourceArnのプロジェクト名はまだ作成していないので、
ここで付けた名前をCodeBuildを作成するときのプロジェクト名にしてください(今回でいうと「dev-tech-press」)

3-2.buildspec.ymlの作成

まずはビルドが成功しているかどうかを見たいので、artifacts以下はコメントアウトしております。
デプロイフェーズで再度コメントアウトを外します

tech_press/buildspec.yml
version: 0.2
env:
  variables:
    DOCKERHUB_USER: "<Docker Hubのユーザー名>"
    DOCKERHUB_PASS: "<Docker Hubのパスワード>"

phases:
  install:
    runtime-versions:
      docker: 20
  pre_build:    
    commands:
      # ECR へのログイン
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --no-include-email --region ap-northeast-1)
      # DockerHub へのログイン(pull回数制限回避のため)
      - echo Logging in to Docker Hub...
      # ID/PWはAWS Systems Manager > パラメータストアを参照
      - echo ${DOCKERHUB_PASS} | docker login -u ${DOCKERHUB_USER} --password-stdin
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - TAG_HOST=<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com
      - TAG_NAME=dev-tech-press-api
      - LOCAL_IMAGE_NAME=tech-press-api
      - TAG_VERSION=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 8)
      - docker-compose --env-file .env.dev up -d
      - docker tag ${LOCAL_IMAGE_NAME}:latest ${TAG_HOST}/${TAG_NAME}:${TAG_VERSION}
      - docker tag ${LOCAL_IMAGE_NAME}:latest ${TAG_HOST}/${TAG_NAME}:latest
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker images
      - docker push ${TAG_HOST}/${TAG_NAME}:${TAG_VERSION}
      - docker push ${TAG_HOST}/${TAG_NAME}:latest
      - printf '{"ImageURI":"%s"}' ${TAG_HOST}/$TAG_NAME:latest > imageDetail.json
# artifacts:
#     files:
#       - ${TASK_DEF_PATH}
#       - ${APPSPEC_PATH}

3-3.CodeBuildの構築

次にBuild環境の構築をしていきます。
CodeBuild -> 「ビルドプロジェクトを作成する」でビルドプロジェクトを作成します。
コメント 2023-01-24 140426.png

プロジェクト名は「dev-tech-press」です。
※先ほど作成したECRリポジトリのポリシーJSONのプロジェクト名と一致させてください
コメント 2023-01-24 140437.png

ソースは先ほど作成した「tech-perss」リポジトリを選択します。
ブランチは現時点では「master」ブランチのままにしておき、後ほど変更します。
コメント 2023-01-24 142714.png

環境はマネージド型イメージを使用します。
他、細かい部分は画像にある通りです。ロール名はデフォルトのまま変更していません。

コメント 2023-01-24 140841.png

Buildspecでは、「buildspecファイルを使用する」を選択し、Buildspec名は「buildspec.yml」と記載します。

その他の設定はデフォルトのままで、「ビルドプロジェクトを作成する」を押下します。
コメント 2023-01-24 141036.png

ビルドプロジェクトが作成されたので、一度「ビルドを開始」を押下して、ビルドを開始しましょう。
すると、以下の画像のようにビルドが失敗します。

エラー文
COMMAND_EXECUTION_ERROR: Error while executing command: $(aws ecr get-login --no-include-email --region ap-northeast-1). Reason: exit status 255

コメント 2023-01-24 153614.png

理由は「環境」で新しく作成したロールにECRのポリシーが設定されていないためです。
コメント 2023-01-24 143233.png

先ほど作成したロール(codebuild-dev-tech-press-service-role)に「AmazonEC2ContainerRegistryPowerUser」ポリシーを追加します。
コメント 2023-01-24 143458.png

この状態になっていればOKです。
再度、ビルドを開始します。
コメント 2023-01-24 153914.png

成功していればビルドフェーズはOKです。

3-4.ECS on Fargate + CodeDeployの構築

3-4-1. クラスターの作成

まずはクラスターを作成します。
Elastic Container Service -> クラスター -> クラスターの作成 でクラスター作成画面に遷移します。

「ネットワーキングのみ」を選択して「次のステップ」を押下します
コメント 2023-01-24 154451.png

クラスター名は「dev-tech-press」とし、その他はデフォルトのままとします。
コメント 2023-01-24 154613.png

「作成」を押下するとクラスターができます
コメント 2023-01-24 154757.png

3-4-2. タスク定義の作成

次に新しいタスク定義を作成します。
Elastic Container Service -> タスク定義 -> 新しいタスク定義の作成 で新しいタスク定義を作成します。
Fargateを選択し、「次のステップ」を押下します。

コメント 2023-01-24 161520.png

タスク定義名は「dev-tech-press」とし、オペレーティングシステムファミリーは「Linux」を選択します。
コメント 2023-01-24 161451.png

タスクサイズはタスクメモリを「0.5GB」・タスクCPUを「0.25vCPU」とします。
※必要に応じてスペックを調整します

次にコンテナの設定をします。
コメント 2023-01-25 092702.png

コンテナ名はdocker-compose.yamlで定義しているコンテナ名を使用し、イメージは「+:latest」とします。
ポートマッピングは80番に設定します。
そのほかの設定はデフォルトのままで「追加」を押下します。

その他のタスク定義もデフォルトのままで「作成」を押下します。

タスク定義が完成したら、タスク定義 -> <作成したタスク定義名> -> <作成したタスク定義名>:1 -> JSON を押下します
背景がグレーになっている部分がタスク定義のJSONなので、pipeline/dev 直下にtaskdef.jsonを作成し、背景がグレーになっている部分をコピーし、codecommitにプッシュします。

3-4-3. サービス・CodeDeployの作成

次にサービスを作成し、その過程でCodeDeployも同時に作成します。
まずは、サービスにアタッチするロールにCodeDeployの権限を付与する必要があります。
IAM -> ロール -> ロールの作成 -> でロールの作成画面に遷移します。そこから、
AWSのサービス -> ユースケースを表示するサービスを選択する -> 「CodeDeploy」と検索 -> 「CodeDeploy - ECS」をチェック -> 次へ -> 次へ とします。
ロール名は「TechPressCodeDeployRoleForECS」とします。
コメント 2023-01-24 183036.png

「ロール名を作成」でロールを作成します。

次に、サービスを作成します。
Elastic Container Service -> 先ほど作成したクラスター(今回でいうと「dev-tech-press」) -> 「サービス」タブを選択した状態で「作成」を押下 でサービスの作成画面に遷移します。
コメント 2023-01-24 184148.png

サービスの設定は
起動タイプ:Fargate
タスク定義:dev-tech-press
サービス名:dev-tech-press-service
タスク数:1
と、そのほかはデフォルトで設定します。

コメント 2023-01-24 184202.png

デプロイメントは
・Blue/Greenデプロイメントを選択
・CodeDeployのサービスロールは先ほど作成した「TechPressCodeDeployRoleForECS」を選択します。

VPCとセキュリティグループはこの記事で作成していないので、割愛します。
パブリック IP の自動割り当ては「ENABLED」を選択します。

ロードバランシングはALBを選択します。

コメント 2023-01-24 185047.png

ここでロードバランサを作成する場合は、ロードバランサのリスナーの設定をなしにしましょう。(サービス作成時に自動的に作成されるため)
コメント 2023-01-24 185214.png

また、ロードバランシングの上にあるヘルスチェックもここで変更します。
コメント 2023-01-24 185442.png

次にロードバランス用のコンテナで「ロードバランサーに追加」を押下し、
コメント 2023-01-24 185522.png

ロードバランス用のコンテナのプロダクションリスナーポートを「80」に設定します。
コメント 2023-01-24 185634.png

Additional configurationは
ターゲットグループ 1 の名前:dev-tech-press-01
ターゲットグループ 2 の名前:dev-tech-press-02
にし、そのほかはデフォルトのまま設定し、「次のステップ」を押下します
コメント 2023-01-24 190001.png

Auto Scalingは特に設定しません。次のステップに行きます

内容を確認し、「サービスの作成」を押下します。

7つのサービス作成ステータスが成功していればOKです。
コメント 2023-01-24 190247.png
コメント 2023-01-24 190256.png

しばらくすると、サービスにてタスクが実行されています。
コメント 2023-01-24 190426.png

タスク画面に遷移すると、
コメント 2023-01-25 093650.png

パブリックIPが見れます。
このIPアドレスを開くと
http://<パブリックIP>/check

コメント 2023-01-25 093802.png

開発環境の環境変数である「Devel」が反映されていればOKです。

3-4-4. CodePipelineの構築

開発環境の最後にCodePipelineの構築をします。
CodePipeline -> パイプラインを作成する でパイプラインの構築画面に遷移します。
パイプラインの名前を「dev-tech-press」とします。
コメント 2023-01-24 191244.png

ソースステージは
・ソースプロバイダー:AWS CodeCommit
・リポジトリ名:tech-press
・ブランチ名:master
とします。
※ブランチは後ほど開発用を作成します。
コメント 2023-01-24 191358.png

ビルドステージは
・プロバイダー:AWS CodeBuild
・プロジェクト名:dev-tech-press
・環境変数を
TECH_PRESS_ECR_NAME:dev-tech-press-api
ENV_FILE:./.env.dev
TASK_DEF_PATH:pipeline/dev/taskdef.json
APPSPEC_PATH:pipeline/dev/appspec.yml
と設定します。
コメント 2023-01-24 192033.png

デプロイステージは
・デプロイプロバイダー:Amazon ECS(ブルー/グリーン)
・アプリケーション名:サービス作成時に構築されたもの
・デプロイグループ:サービス作成時に構築されたもの
Amazon ECS タスク定義は「BuildArtifact」を選択し、「pipeline/dev/taskdef.json」
AWS CodeDeploy AppSpec ファイルは「BuildArtifact」を選択し、「pipeline/dev/appspec.yml」とします。
コメント 2023-01-24 192509.png

最後にレビューを確認し、パイプラインを作成します。

作成した後、パイプラインが自動で起動しますが、この段階ではDeploy時にエラーが発生します。理由はbuildspec.ymlがビルド段階までのソースになっているからです。
buildspec.ymlを変更します

tech_press/buildspec.yml
version: 0.2
env:
  variables:
    DOCKERHUB_USER: "<Docker Hubのユーザー名>"
    DOCKERHUB_PASS: "<Docker Hubのパスワード>"

phases:
  install:
    runtime-versions:
      docker: 20
  pre_build:    
    commands:
      # ECR へのログイン
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --no-include-email --region ap-northeast-1)
      # DockerHub へのログイン(pull回数制限回避のため)
      - echo Logging in to Docker Hub...
      # ID/PWはAWS Systems Manager > パラメータストアを参照
      - echo ${DOCKERHUB_PASS} | docker login -u ${DOCKERHUB_USER} --password-stdin
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - TAG_HOST=<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com
      - TAG_NAME=${TECH_PRESS_ECR_NAME}
      - LOCAL_IMAGE_NAME=tech-press-api
      - TAG_VERSION=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 8)
      - docker-compose --env-file ${ENV_FILE} up -d
      - docker tag ${LOCAL_IMAGE_NAME}:latest ${TAG_HOST}/${TAG_NAME}:${TAG_VERSION}
      - docker tag ${LOCAL_IMAGE_NAME}:latest ${TAG_HOST}/${TAG_NAME}:latest
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker images
      - docker push ${TAG_HOST}/${TAG_NAME}:${TAG_VERSION}
      - docker push ${TAG_HOST}/${TAG_NAME}:latest
      - printf '{"ImageURI":"%s"}' ${TAG_HOST}/$TAG_NAME:latest > imageDetail.json
artifacts:
    files:
      - ${TASK_DEF_PATH}
      - ${APPSPEC_PATH}

buildspec.ymlを変更し、プッシュし、再度「dev-tech-press」パイプラインを走らせます。
コメント 2023-01-24 195236.png

この段階でパイプラインが成功していれば、一旦開発パイプラインの構築は完了です。

4.本番環境パイプラインを構築

開発環境と構築方法は同じなので、変更している名前等を書き記します。

4-1.ECRのリポジトリ作成

リポジトリ名:product-tech-press-api

4-2.buildspec.ymlの作成

ここで作成するbuildspec.ymlは3-4-4.で作成したものでOKなので何もしなくて大丈夫です

4-3.CodeBuildの構築

プロジェクト名:product-tech-press
他の設定は3-3と同様です
また、この段階で「ビルドを開始」は不要です

4-4.ECS on Fargate + CodeDeployの構築

4-4-1. クラスターの作成

クラスター名:product-tech-press

4-4-2. タスク定義の作成

タスク定義名:product-tech-press
イメージ:「<本番ECRリポジトリのURI>+:latest」

4-4-3. サービス・CodeDeployの作成

ロールは3-4-3で作成した「「TechPressCodeDeployRoleForECS」でOKです
タスク定義:product-tech-press
サービス名:product-tech-press-service
ロードバランサ名:product-tech-press
ターゲットグループ 1 の名前:product-tech-press-01
ターゲットグループ 2 の名前:product-tech-press-02
この段階でサービスは立ち上がりますが、タスクは起動→停止を繰り返します。
パイプラインを構築することで、解消されます。

4-4-4. CodePipelineの構築

ビルドステージ
プロジェクト名:product-tech-press
環境変数を
TECH_PRESS_ECR_NAME:product-tech-press-api
ENV_FILE:./.env.product
TASK_DEF_PATH:pipeline/product/taskdef.json
APPSPEC_PATH:pipeline/product/appspec.yml

デプロイステージ
AWS CodeDeploy アプリケーション名:AppECS-product-tech-press-product-tech-press-service

AWS CodeDeploy デプロイグループ:DgpECS-product-tech-press-product-tech-press-service

3-4と同様に構築ができれば最終的にパイプラインが成功するかと思います。
Elastic Container Service -> 本番用クラスター -> サービス名 -> タスク で、タスクが正常に動いていたらOKです
コメント 2023-01-24 204911.png

前回のステータスが「RUNNING」が作動していたらタスクが正常に動いています。
本番用のパブリックIPで本番用の環境変数が適用されているか確認します。
コメント 2023-01-25 094729.png

問題なく動いているので、本番用のパイプラインの構築も完了です

5.パイプラインのトリガーをブランチ毎に変更

最後に、今の状態だと両パイプラインがmasterブランチが変更するとパイプラインが起動する設定になっています。
「staging」ブランチと「production」ブランチを作成し、「staging」ブランチに変更がかかると開発パイプラインが、「production」ブランチに変更がかかると本番パイプラインが起動する設定に変更します。

まずは、stagingブランチとproductionブランチを作ります
コメント 2023-01-24 210248.png

次に開発のパイプラインの設定をします。
CodePipeline -> 開発用パイプライン -> 編集 -> Sourceの「ステージを編集する」 -> 編集のアイコンをクリック -> ブランチ名を「staging」に変更 -> 完了 -> 保存
コメント 2023-01-24 210635.png

本番パイプラインも設定をします。
コメント 2023-01-24 210832.png

これで「staging」ブランチに変更がかかると開発パイプラインが、「production」ブランチに変更がかかると本番パイプラインが起動する設定に変更されました。

最後に実際に変更がかかるのか、プルリクエストをして確認します。
まずは開発機の変更を確認するためのブランチ、「develop/開発パイプライン確認ブランチ」を作成します。
「develop/開発パイプライン確認ブランチ」のブランチには以下の変更を加えます

tech_press/.env.dev
# COMMON
TIMEZONE=Asia/Tokyo

# API
API_HOST=0.0.0.0
API_PORT=8080

- CONTAINER_ENV=Devel
+ CONTAINER_ENV=開発

プッシュして、プルリクエストを「staging」ブランチに投げ、マージします
コメント 2023-01-24 211455.png

パイプラインが終了すると、環境変数が変更されているのが分かります。
コメント 2023-01-25 095750.png

同様に「develop/本番パイプライン確認ブランチ」を作成し、.env.productを変更し、「production」にマージします

tech_press/.env.product
# COMMON
TIMEZONE=Asia/Tokyo

# API
API_HOST=0.0.0.0
API_PORT=8080

- CONTAINER_ENV=Product
+ CONTAINER_ENV=本番

本番も反映されていますね。めでたしめでたし。

コメント 2023-01-25 095833.png

おわりに

気が付いたら、中々のボリュームになっていましたw
今回は省きましたが、所属しているチームではパイプラインにテストを入れていたり、承認を入れたりして各フェーズでのチェックを行っています。
CI/CDは構築されたらその後は基本的に触らないため、今回の執筆でも思い出す部分が多かったです。

まだまだ1年目で浅くしか経験していないので、知識をより深めていきたいです!

10
8
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
10
8