0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub Actions + Docker + AWS EC2でWebアプリのCI/CDパイプラインを構築し自動デプロイする

0
Last updated at Posted at 2026-02-26

はじめに

前回の記事では、Dockerfile を使って Flask + MySQL アプリケーションをコンテナ化しました

しかし、コードを変更するたびに手動でビルド・デプロイするのは非効率です

この記事では、GitHub Actions を使って、コードを push するだけで自動的にビルド・テスト・デプロイが実行される CI/CD パイプラインを構築します

この記事で分かること

  • CI/CD とは何か、なぜ必要なのか
  • GitHub Actions の基本的な使い方
  • Docker イメージの自動ビルドと Docker Hub への push
  • 自動テストの実行方法
  • AWS EC2 への自動デプロイ
  • 実践:Flask アプリの完全な CI/CD パイプライン構築

前提条件

本記事は、以下が完了していることを前提としています

  • GitHub アカウントを持っている
  • Docker Hub アカウントを持っている
  • AWS アカウントを持っている(無料枠で OK)
  • 基本的な Git の操作ができる
  • Docker と Docker Compose の基本を理解している

前提記事:

なお、本記事で使用するソースコードは、以下のGitHubリポジトリからダウンロードできます
flask-pipeline-app(GitHubリポジトリ)

CI/CD とは?

従来の開発フロー(手動)

コード変更
  ↓
手動でテスト実行
  ↓
手動でビルド
  ↓
手動でサーバーにデプロイ
  ↓
動作確認

問題点:

  • 時間がかかる
  • 人的ミスが発生しやすい
  • デプロイが億劫になる

CI/CD の開発フロー(自動)

コード変更 → Git push
  ↓
自動でテスト実行
  ↓
自動でビルド
  ↓
自動でデプロイ
  ↓
自動で通知

メリット:

  • 高速なフィードバック
  • 人的ミスの削減
  • 頻繁なデプロイが可能

CI/CD の用語

用語 説明
CI (Continuous Integration) 継続的インテグレーション コードを push したら自動テスト
CD (Continuous Delivery) 継続的デリバリー テスト通過後、自動でビルド
CD (Continuous Deployment) 継続的デプロイメント ビルド後、自動で本番環境へデプロイ

GitHub Actions とは?

GitHub が提供する CI/CD プラットフォームです

主な特徴

  • GitHub に統合されている(追加設定不要)
  • YAML ファイルで設定
  • 豊富なアクション(再利用可能な処理)
  • 無料枠あり(パブリックリポジトリは無制限)

GitHub Actions の基本構造

name: ワークフロー名

on:
  push:
    branches: [ main ]  # トリガー(main ブランチへの push)

jobs:
  job-name:
    runs-on: ubuntu-latest  # 実行環境
    steps:
      - name: ステップ名
        uses: actions/checkout@v3  # アクションの使用
      - name: コマンド実行
        run: echo "Hello World"  # コマンドの実行

実践:Flask アプリの CI/CD パイプライン構築

今回の構成

この記事では、以下の構成で CI/CD パイプラインを構築します

[開発者]
  ↓ Git push
[GitHub]
  ↓ トリガー
[GitHub Actions] ← CI/CD エンジン(テスト・ビルド・デプロイを自動実行)
  ↓ Docker イメージを push
[Docker Hub] ← イメージレジストリ(ビルドしたイメージを保管)
  ↓ イメージを pull
[AWS EC2] ← 単なる Linux サーバー(Docker が動く環境)
  ↓
[ユーザー] ← ブラウザでアクセス

AWS の役割

項目 説明
使用するもの EC2(仮想マシン)のみ
役割 Docker と Docker Compose が動く Linux 環境を提供
位置づけ 単なる Web サーバー

AWS 固有の機能は使用しない

以下の AWS サービスは使用しません

  • ❌ AWS CodePipeline(CI/CD サービス)
  • ❌ AWS CodeBuild(ビルドサービス)
  • ❌ AWS CodeDeploy(デプロイサービス)
  • ❌ ECS/Fargate(コンテナオーケストレーション)
  • ❌ ECR(Docker イメージレジストリ)

理由
シンプルで理解しやすい
無料枠で完結
他の VPS サービスでも同じ構成が可能

※AWS 無料利用枠の注意点
t2.micro インスタンス: 月 750 時間まで無料(1インスタンスを常時稼働可能)
ストレージ: 30 GB まで無料
データ転送: 月 15 GB まで無料(アウトバウンド)
無料期間: AWS アカウント作成から 12 ヶ月間

EC2 の代替サービス

Docker が動く Linux サーバーがあれば、EC2 の代わりに以下も使用可能です

  • さくらの VPS
  • ConoHa VPS
  • DigitalOcean Droplet
  • Oracle Cloud(永久無料)

完成イメージ

Git push (main ブランチ)
  ↓
GitHub Actions 起動
  ↓
① コードのチェックアウト
  ↓
② Python 環境のセットアップ
  ↓
③ 依存パッケージのインストール
  ↓
④ テストの実行
  ↓
⑤ Docker イメージのビルド
  ↓
⑥ Docker Hub へ push
  ↓
⑦ デプロイ(AWS EC2)
  ↓
完了通知

ステップ1:プロジェクトの準備

ディレクトリ構成

以下の構成でプロジェクトを作成します

flask-pipeline-app/
├── .github/
│   └── workflows/
│       └── ci-cd.yml
├── app/
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── app.py
│   ├── test_app.py
│   └── templates/
│       └── index.html
├── docker-compose.yml
├── docker-compose.prod.yml
└── init.sql

プロジェクトディレクトリの作成

# プロジェクトディレクトリを作成
mkdir flask-pipeline-app
cd flask-pipeline-app

# サブディレクトリを作成
mkdir app
mkdir app\templates
mkdir .github
mkdir .github\workflows

ディレクトリとファイルの作成

# プロジェクトディレクトリに移動(既に作成済み)
cd flask-pipeline-app

# 各ファイルを作成
# Windows の場合
type nul > .github\workflows\ci-cd.yml
type nul > app\Dockerfile
type nul > app\requirements.txt
type nul > app\app.py
type nul > app\test_app.py
type nul > app\templates\index.html
type nul > docker-compose.yml
type nul > docker-compose.prod.yml
type nul > init.sql

# または、エディタで直接作成
code .

これから各ファイルの内容を作成していきます

ステップ2:Webアプリケーション(Flask版)の作成

app/requirements.txt

Python の依存パッケージを定義します

Flask==3.0.0
mysql-connector-python==8.2.0
pytest==7.4.3
pytest-cov==4.1.0

app/app.py

Flask アプリケーションのメインファイルです

from flask import Flask, render_template, request, redirect, url_for
import mysql.connector
import os
import time

app = Flask(__name__)

# MySQL 接続設定(環境変数から取得)
db_config = {
    'host': os.getenv('DB_HOST', 'db'),
    'user': os.getenv('DB_USER', 'flaskuser'),
    'password': os.getenv('DB_PASSWORD', 'flaskpass'),
    'database': os.getenv('DB_NAME', 'flaskdb')
}

def get_db_connection():
    """MySQL への接続を取得(リトライ機能付き)"""
    max_retries = 5
    retry_interval = 2
    
    for attempt in range(max_retries):
        try:
            conn = mysql.connector.connect(**db_config)
            return conn
        except mysql.connector.Error as err:
            if attempt < max_retries - 1:
                print(f"データベース接続失敗(試行 {attempt + 1}/{max_retries}): {err}")
                time.sleep(retry_interval)
            else:
                raise

@app.route('/')
def index():
    """ユーザー一覧を表示"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        cursor.execute('SELECT * FROM users ORDER BY created_at DESC')
        users = cursor.fetchall()
        cursor.close()
        conn.close()
        return render_template('index.html', users=users, error=None)
    except Exception as e:
        return render_template('index.html', users=[], error=str(e))

@app.route('/add', methods=['POST'])
def add_user():
    """新規ユーザーを追加"""
    name = request.form.get('name')
    email = request.form.get('email')
    
    if not name or not email:
        return redirect(url_for('index'))
    
    try:
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute(
            'INSERT INTO users (name, email) VALUES (%s, %s)',
            (name, email)
        )
        conn.commit()
        cursor.close()
        conn.close()
    except Exception as e:
        print(f"ユーザー追加エラー: {e}")
    
    return redirect(url_for('index'))

@app.route('/delete/<int:user_id>')
def delete_user(user_id):
    """ユーザーを削除"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute('DELETE FROM users WHERE id = %s', (user_id,))
        conn.commit()
        cursor.close()
        conn.close()
    except Exception as e:
        print(f"ユーザー削除エラー: {e}")
    
    return redirect(url_for('index'))

@app.route('/health')
def health():
    """ヘルスチェック用エンドポイント"""
    try:
        conn = get_db_connection()
        conn.close()
        return {'status': 'healthy', 'database': 'connected'}, 200
    except Exception as e:
        return {'status': 'unhealthy', 'error': str(e)}, 503

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

app/templates/index.html

ユーザーインターフェースです(前回の記事と同じ内容のため省略)

詳細は 前提記事:Dockerfile を使った独自イメージの作成 を参照してください

app/Dockerfile

Flask アプリケーションをコンテナ化します

# Python 3.11 の軽量イメージをベースに使用
FROM python:3.11-slim

# 作業ディレクトリを設定
WORKDIR /app

# 依存パッケージファイルをコピー
COPY requirements.txt .

# Python パッケージをインストール
RUN pip install --no-cache-dir -r requirements.txt

# アプリケーションファイルをコピー
COPY . .

# Flask が使用するポートを公開
EXPOSE 5000

# コンテナ起動時に Flask アプリを実行
CMD ["python", "app.py"]

ステップ3:MySQL 初期化スクリプトの作成

init.sql

データベースとテーブルを自動作成します

-- データベースが存在しない場合は作成
CREATE DATABASE IF NOT EXISTS flaskdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- データベースを使用
USE flaskdb;

-- users テーブルを作成
CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_email (email),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- サンプルデータを挿入
INSERT INTO users (name, email) VALUES
    ('YamadaTaro', 'taro@example.com'),
    ('SatoHanako', 'hanako@example.com'),
    ('SuzukiJiro', 'jiro@example.com')
ON DUPLICATE KEY UPDATE name=name;

ステップ4:Docker Compose の設定

docker-compose.yml

開発環境用の設定です

version: '3.8'

services:
  db:
    image: mysql:8.0
    container_name: flask-mysql-db
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: flaskdb
      MYSQL_USER: flaskuser
      MYSQL_PASSWORD: flaskpass
      FLASK_ENV: development
    volumes:
      - db-data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - flask-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpass"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: always

  web:
    build:
      context: ./app
      dockerfile: Dockerfile
    container_name: flask-web
    ports:
      - "5000:5000"
    environment:
      DB_HOST: db
      DB_USER: flaskuser
      DB_PASSWORD: flaskpass
      DB_NAME: flaskdb
    volumes:
      - ./app:/app
    networks:
      - flask-network
    depends_on:
      db:
        condition: service_healthy
    restart: always

volumes:
  db-data:

networks:
  flask-network:
    driver: bridge

ステップ5:テストコードの作成

app/test_app.py

Flask アプリケーションのテストを作成します

import pytest
from app import app
import os

@pytest.fixture
def client():
    """テスト用のクライアントを作成"""
    app.config['TESTING'] = True
    
    # テスト用の環境変数を設定
    os.environ['DB_HOST'] = 'localhost'
    os.environ['DB_USER'] = 'testuser'
    os.environ['DB_PASSWORD'] = 'testpass'
    os.environ['DB_NAME'] = 'testdb'
    
    with app.test_client() as client:
        yield client

def test_health_endpoint(client):
    """ヘルスチェックエンドポイントのテスト"""
    response = client.get('/health')
    
    # ステータスコードが 200 または 503 であることを確認
    assert response.status_code in [200, 503]
    
    # JSON レスポンスであることを確認
    assert response.content_type == 'application/json'
    
    # status フィールドが存在することを確認
    data = response.get_json()
    assert 'status' in data

def test_index_page(client):
    """トップページのテスト"""
    response = client.get('/')
    
    # ステータスコードが 200 であることを確認
    assert response.status_code == 200
    
    # HTML が返されることを確認
    assert b'<!DOCTYPE html>' in response.data

def test_add_user_validation(client):
    """ユーザー追加のバリデーションテスト"""
    # 空のデータで POST
    response = client.post('/add', data={})
    
    # リダイレクトされることを確認
    assert response.status_code == 302

ステップ6:本番用 Docker Compose の作成

docker-compose.prod.yml

本番環境用の設定を作成します

version: '3.8'

services:
  db:
    image: mysql:8.0
    container_name: flask-mysql-db-prod
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      FLASK_ENV: production
    volumes:
      - db-data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - flask-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: always

  web:
    image: ${DOCKER_USERNAME}/flask-pipeline-app:latest  # Docker Hub から取得
    container_name: flask-web-prod
    ports:
      - "80:5000"
    environment:
      DB_HOST: db
      DB_USER: ${MYSQL_USER}
      DB_PASSWORD: ${MYSQL_PASSWORD}
      DB_NAME: ${MYSQL_DATABASE}
      FLASK_ENV: production
    networks:
      - flask-network
    depends_on:
      db:
        condition: service_healthy
    restart: always

volumes:
  db-data:

networks:
  flask-network:
    driver: bridge

ステップ7:GitHub Actions ワークフローの作成

.github/workflows/ci-cd.yml

CI/CD パイプラインを定義します

name: Flask App CI/CD

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  DOCKER_IMAGE_NAME: flask-pipeline-app

jobs:
  test:
    name: テストの実行
    runs-on: ubuntu-latest
    
    steps:
      - name: コードのチェックアウト
        uses: actions/checkout@v3
      
      - name: Python 環境のセットアップ
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
          cache: 'pip'
      
      - name: 依存パッケージのインストール
        run: |
          cd app
          pip install -r requirements.txt
      
      - name: テストの実行
        run: |
          cd app
          pytest test_app.py -v --cov=app --cov-report=term-missing
      
      - name: テスト結果のアップロード
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: app/.coverage

  build:
    name: Docker イメージのビルドと push
    runs-on: ubuntu-latest
    needs: test  # test ジョブが成功したら実行
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
    steps:
      - name: コードのチェックアウト
        uses: actions/checkout@v3
      
      - name: Docker Buildx のセットアップ
        uses: docker/setup-buildx-action@v2
      
      - name: Docker Hub へログイン
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      
      - name: Docker イメージのビルドと push
        uses: docker/build-push-action@v4
        with:
          context: ./app
          file: ./app/Dockerfile
          push: true
          tags: |
            ${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:latest
            ${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:${{ github.sha }}
          cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:buildcache
          cache-to: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:buildcache,mode=max
      
      - name: イメージ情報の出力
        run: |
          echo "Image: ${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:latest"
          echo "SHA: ${{ github.sha }}"

  deploy:
    name: 本番環境へのデプロイ
    runs-on: ubuntu-latest
    needs: build  # build ジョブが成功したら実行
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
    steps:
      - name: コードのチェックアウト
        uses: actions/checkout@v3
      
      - name: SSH 秘密鍵の設定
        run: |
          mkdir -p ~/.ssh
          cat << 'EOF' > ~/.ssh/id_rsa
          ${{ secrets.SSH_PRIVATE_KEY }}
          EOF
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts
      
      - name: サーバーへファイルをコピー
        run: |
          scp docker-compose.prod.yml init.sql ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:~/flask-app/
      
      - name: サーバーでデプロイを実行
        run: |
          ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF'
            cd ~/flask-app
            
            # 環境変数を設定
            export DOCKER_USERNAME=${{ secrets.DOCKER_USERNAME }}
            export MYSQL_ROOT_PASSWORD=${{ secrets.MYSQL_ROOT_PASSWORD }}
            export MYSQL_DATABASE=${{ secrets.MYSQL_DATABASE }}
            export MYSQL_USER=${{ secrets.MYSQL_USER }}
            export MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }}
            
            # 最新のイメージを取得
            docker pull ${{ secrets.DOCKER_USERNAME }}/flask-pipeline-app:latest
            
            # コンテナを再起動
            docker-compose -f docker-compose.prod.yml down
            docker-compose -f docker-compose.prod.yml up -d
            
            # 古いイメージを削除
            docker image prune -f
          EOF
      
      - name: デプロイ完了通知
        run: |
          echo "デプロイが完了しました"
          echo "URL: http://${{ secrets.SERVER_HOST }}"

ステップ8:GitHub Secrets の設定

GitHub リポジトリの Settings → Secrets and variables → Actions で以下の情報を登録します

このステップでは、以下の作業を行います

  1. Docker Hub アクセストークンの作成
  2. AWS EC2 インスタンスのセットアップ
  3. GitHub Actions 用 SSH 鍵の設定
  4. GitHub Secrets への登録

8-1. Docker Hub アクセストークンの作成

  1. Docker Hub にログイン
  2. Account Settings → Personal access tokens → [Generate New Token]をクリック
  3. トークン名を入力(例:flask-pipeline-github-actions
  4. 権限を選択(Read, Write, Delete)
  5. Generate をクリック
  6. 表示されたトークンをコピー(再表示不可)

8-2. AWS EC2 インスタンスのセットアップ

1. EC2 インスタンスの作成

AWS マネジメントコンソールでの操作

  1. AWS マネジメントコンソール にログイン
  2. サービスから「EC2」を選択
  3. 「インスタンスを起動」ボタンをクリック

インスタンスの設定

項目 設定値 説明
名前 flask-pipeline-server 任意の名前
AMI Amazon Linux 2023 AMI 無料利用枠の対象
アーキテクチャ 64 ビット (x86) デフォルト
インスタンスタイプ t2.micro 無料利用枠の対象
キーペア 新規作成または既存 SSH 接続用(後述)
ネットワーク設定 デフォルト VPC そのまま
セキュリティグループ 新規作成(flask-pipeline-sg) ルールを追加(後述)
ストレージ 8 GB (gp3) 無料利用枠の範囲内

セキュリティグループの設定

以下のインバウンドルールを追加します:

タイプ プロトコル ポート範囲 ソース 説明
SSH TCP 22 0.0.0.0/0 SSH 接続用
HTTP TCP 80 0.0.0.0/0 Web アクセス用
カスタム TCP TCP 5000 0.0.0.0/0 Flask アプリ用(開発時)

キーペアの作成に関して(初回のみ)

  1. 「新しいキーペアの作成」を選択
  2. キーペア名:flask-pipeline-ec2-key
  3. キーペアのタイプ:RSA
  4. プライベートキーファイル形式:.pem
  5. 「キーペアを作成」をクリック
  6. ダウンロードされた flask-pipeline-ec2-key.pem を安全な場所に保存

重要: このキーペアは EC2 への SSH 接続用です(GitHub Actions 用の SSH 鍵とは別)

上記の設定を完了した後、「インスタンスを起動」ボタンをクリックして、EC2インスタンスを起動します

2. EC2 インスタンスへの接続

インスタンスの IP アドレスを確認

  1. EC2 ダッシュボードで「インスタンス」を選択
  2. 作成したインスタンスをクリック
  3. 「パブリック IPv4 アドレス」をメモ(例:54.123.45.67

SSH で接続

# Windows PowerShell で実行

# キーファイルのパーミッションを設定(初回のみ)
icacls flask-pipeline-ec2-key.pem /inheritance:r
icacls flask-pipeline-ec2-key.pem /grant:r "%USERNAME%:R"

# EC2 に接続(Amazon Linux 2023 のデフォルトユーザーは ec2-user)
ssh -i flask-pipeline-ec2-key.pem ec2-user@54.123.45.67

# 初回接続時は "yes" を入力

接続できれば、以下のようなプロンプトが表示されます

[ec2-user@ip-172-31-xx-xx ~]$

3. EC2 インスタンスに Docker をインストール

EC2 に SSH 接続した状態で以下を実行します

Amazon Linux 2023 では Docker のインストール手順

# システムパッケージを更新
sudo dnf update -y

# Docker をインストール
sudo dnf install -y docker

# Docker サービスを起動
sudo systemctl start docker

# Docker を自動起動に設定
sudo systemctl enable docker

# ec2-user を docker グループに追加
sudo usermod -aG docker ec2-user

# Docker Compose をインストール
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# 設定を反映(再ログインが必要)
exit

再度 SSH で接続します

ssh -i flask-pipeline-ec2-key.pem ec2-user@54.123.45.67

インストール確認

# Docker のバージョン確認
docker --version
# 出力例: Docker version 24.0.7, build afdd53b

# Docker Compose のバージョン確認
docker-compose --version
# 出力例: Docker Compose version v2.23.0

4. デプロイ用ディレクトリの作成

# ホームディレクトリにアプリ用ディレクトリを作成
mkdir -p ~/flask-app
cd ~/flask-app

# 確認
pwd
# 出力: /home/ec2-user/flask-app

8-3. GitHub Actions 用 SSH 鍵の設定

1. ローカル PC (Windows) で GitHub Actions 用 SSH 鍵ペアを生成

重要: これは GitHub Actions が EC2 に自動デプロイするための鍵です(EC2 接続用のキーペアとは別)

# PowerShell で実行
# カレントディレクトリに鍵ファイルが作成されます
ssh-keygen -t rsa -b 4096 -C "flask-pipeline-github-actions" -f github-actions-key

# 実行結果:
# github-actions-key      ← 秘密鍵(GitHub Secrets に登録)
# github-actions-key.pub  ← 公開鍵(サーバーに登録)

パスフレーズを聞かれたら、Enter キーを2回押して空のままにします

2. 公開鍵を EC2 インスタンスに登録

公開鍵の内容を確認

# Windows PowerShell で公開鍵の内容を表示
type github-actions-key.pub

出力例:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC... flask-pipeline-github-actions

この内容をコピーします

EC2 に SSH 接続して公開鍵を登録

# EC2 に接続
ssh -i flask-pipeline-ec2-key.pem ec2-user@54.123.45.67

EC2 上で以下を実行:

# .ssh ディレクトリが存在しない場合は作成
mkdir -p ~/.ssh
chmod 700 ~/.ssh

# authorized_keys ファイルを編集
nano ~/.ssh/authorized_keys

nano エディタが開いたら:

  1. コピーした公開鍵の内容を貼り付け(既存の内容を消さずに追加)
  2. Ctrl + O で保存
  3. Enter で確認
  4. Ctrl + X で終了

パーミッションを設定:

chmod 600 ~/.ssh/authorized_keys

# 確認
cat ~/.ssh/authorized_keys
# 公開鍵の内容が表示されれば OK

# EC2 から一旦ログアウト
exit

3. 秘密鍵を GitHub Secrets に登録

秘密鍵の内容を確認

# Windows PowerShell で秘密鍵の内容を表示
type github-actions-key

出力例:

-----BEGIN RSA PRIVATE KEY-----
MIIJKAIBAAKCAgEAr8... (長い文字列)
...
-----END RSA PRIVATE KEY-----

8-4. GitHub Secrets への登録

上記で取得した情報を GitHub Secrets に登録します

事前準備: GitHub リポジトリの作成

  1. GitHub にログイン
  2. 右上の「+」→「New repository」をクリック
  3. 以下を入力
    • Repository name: flask-pipeline-app
    • Public または Private を選択
    • 「Add a README file」のチェックは外す
  4. 「Create repository」ボタンをクリック

Secrets の登録手順

  1. 作成した GitHub リポジトリのページを開く
  2. 「Settings」タブをクリック
  3. 左メニューから「Secrets and variables」→「Actions」をクリック
  4. 「New repository secret」ボタンをクリック
  5. Name と Secret を入力
  6. 「Add secret」ボタンをクリック
  7. 上記の手順 4~6 を繰り返して、以下の 9 つの Secret を登録

登録する Secret 一覧

Secret 名 値の例 取得方法 使用箇所と役割
DOCKER_USERNAME myusername Docker Hub のユーザー名 Docker Hub へのログインとイメージの push に使用
DOCKER_PASSWORD dckr_pat_xxxxx Docker Hub で生成したアクセストークン(8-1 で作成) Docker Hub への認証に使用
SERVER_HOST 54.123.45.67 EC2 インスタンスのパブリック IPv4 アドレス(8-2 で確認) SSH 接続先サーバーの IP アドレス
SERVER_USER ec2-user Amazon Linux 2023 のデフォルトユーザー SSH 接続時のユーザー名
SSH_PRIVATE_KEY -----BEGIN RSA... github-actions-key ファイルの内容(8-3 で作成) GitHub Actions から EC2 への SSH 接続に使用
MYSQL_ROOT_PASSWORD securepass123 任意の強力なパスワード MySQL の root ユーザーのパスワード(docker-compose.prod.yml で使用)
MYSQL_DATABASE flaskdb データベース名 MySQL で作成するデータベース名(docker-compose.prod.yml で使用)
MYSQL_USER flaskuser MySQL ユーザー名 アプリケーションが使用する MySQL ユーザー(docker-compose.prod.yml で使用)
MYSQL_PASSWORD flaskpass123 任意の強力なパスワード アプリケーション用 MySQL ユーザーのパスワード(docker-compose.prod.yml で使用)

ステップ9:GitHub リポジトリにコードの push

ローカルリポジトリの初期化

# プロジェクトディレクトリに移動
cd flask-pipeline-app

# Git リポジトリを初期化
git init

# .gitignore を作成
echo "__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.env
.vscode/
.coverage
*.log" > .gitignore

# ファイルを追加
git add .

# コミット
git commit -m "Initial commit: Flask + MySQL app with CI/CD"

# リモートリポジトリを追加
git remote add origin https://github.com/yourusername/flask-pipeline-app.git

# main ブランチに push
git branch -M main
git push -u origin main

ステップ10:CI/CD の動作確認

GitHub Actions の確認

  1. GitHub リポジトリの「Actions」タブを開く
  2. ワークフローが自動的に実行される
  3. 各ジョブの進行状況を確認

ワークフローの実行結果

✅ test (テストの実行)
  ├─ コードのチェックアウト
  ├─ Python 環境のセットアップ
  ├─ 依存パッケージのインストール
  ├─ テストの実行
  └─ テスト結果のアップロード

✅ build (Docker イメージのビルドと push)
  ├─ コードのチェックアウト
  ├─ Docker Buildx のセットアップ
  ├─ Docker Hub へログイン
  ├─ Docker イメージのビルドと push
  └─ イメージ情報の出力

✅ deploy (本番環境へのデプロイ)
  ├─ コードのチェックアウト
  ├─ SSH 秘密鍵の設定
  ├─ サーバーへファイルをコピー
  ├─ サーバーでデプロイを実行
  └─ デプロイ完了通知

Docker Hub の確認

  1. Docker Hub にログイン
  2. Repositories を開く
  3. flask-pipeline-app リポジトリが作成されている
  4. latest<commit-sha> のタグが存在する
  5. ブラウザで確認
http://your-server-ip

image.png

ステップ11:コード変更と自動デプロイの確認

コードを変更

# app/app.py の一部を変更
@app.route('/health')
def health():
    """ヘルスチェック用エンドポイント"""
    try:
        conn = get_db_connection()
        conn.close()
        return {
            'status': 'healthy',
            'database': 'connected',
            'version': '1.1.0'  # ← バージョンを追加
        }, 200
    except Exception as e:
        return {'status': 'unhealthy', 'error': str(e)}, 503

変更を push

git add app/app.py
git commit -m "Add version to health endpoint"
git push origin main

自動デプロイの確認

  1. GitHub Actions が自動的に実行される
  2. テスト → ビルド → デプロイが順次実行される
  3. デプロイ完了後、ブラウザで確認
http://your-server-ip/health

レスポンス:

{
  "status": "healthy",
  "database": "connected",
  "version": "1.1.0"
}

高度な設定

1. ブランチ戦略

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    # 全ブランチでテスト実行
    
  build:
    # main ブランチのみビルド
    if: github.ref == 'refs/heads/main'
    
  deploy-staging:
    # develop ブランチはステージング環境へ
    if: github.ref == 'refs/heads/develop'
    
  deploy-production:
    # main ブランチは本番環境へ
    if: github.ref == 'refs/heads/main'

2. 通知の追加

Slack への通知を追加:

- name: Slack 通知
  if: always()
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    text: 'デプロイが完了しました'
    webhook_url: ${{ secrets.SLACK_WEBHOOK }}

3. ロールバック機能

- name: ロールバック
  if: failure()
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF'
      cd ~/flask-app
      docker-compose -f docker-compose.prod.yml down
      docker pull ${{ secrets.DOCKER_USERNAME }}/flask-pipeline-app:previous
      docker-compose -f docker-compose.prod.yml up -d
    EOF

4. 環境ごとの設定

jobs:
  deploy:
    strategy:
      matrix:
        environment: [staging, production]
    environment:
      name: ${{ matrix.environment }}
      url: https://${{ matrix.environment }}.example.com

トラブルシューティング

テストが失敗する

# ローカルでテストを実行
cd app
pytest test_app.py -v

# カバレッジを確認
pytest test_app.py --cov=app --cov-report=html

Docker イメージの push が失敗する

Error: denied: requested access to the resource is denied

対処法:

  1. Docker Hub のアクセストークンを確認
  2. GitHub Secrets の DOCKER_USERNAMEDOCKER_PASSWORD を確認
  3. Docker Hub でリポジトリが作成されているか確認

SSH 接続が失敗する

Permission denied (publickey)

対処法:

  1. SSH 秘密鍵が正しく設定されているか確認
  2. サーバーの ~/.ssh/authorized_keys に公開鍵が登録されているか確認
  3. サーバーのファイアウォール設定を確認

デプロイ後にアプリが起動しない

# サーバーにログイン
ssh user@server-ip

# ログを確認
cd ~/flask-app
docker-compose -f docker-compose.prod.yml logs

# コンテナの状態を確認
docker-compose -f docker-compose.prod.yml ps

ベストプラクティス

1. Secrets の管理

  • パスワードは必ず GitHub Secrets に保存
  • .env ファイルは .gitignore に追加
  • 定期的にパスワードをローテーション

2. テストの充実

# app/test_app.py にテストを追加
def test_add_user_success(client, mocker):
    """ユーザー追加の成功テスト"""
    # データベース接続をモック
    mock_conn = mocker.patch('app.get_db_connection')
    
    response = client.post('/add', data={
        'name': 'Test User',
        'email': 'test@example.com'
    })
    
    assert response.status_code == 302

3. イメージのタグ戦略

tags: |
  ${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:latest
  ${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:${{ github.sha }}
  ${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:v1.0.0

4. キャッシュの活用

- name: Docker レイヤーキャッシュ
  uses: actions/cache@v3
  with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-buildx-

5. セキュリティスキャン

- name: Trivy によるセキュリティスキャン
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ${{ secrets.DOCKER_USERNAME }}/flask-pipeline-app:latest
    format: 'sarif'
    output: 'trivy-results.sarif'

まとめ

  • GitHub Actions で CI/CD パイプラインを構築
  • コードを push するだけで自動テスト・ビルド・デプロイ
  • Docker Hub にイメージを自動 push
  • SSH 経由でサーバーに自動デプロイ
  • Secrets で機密情報を安全に管理

次のステップ

  • Kubernetes へのデプロイ
  • AWS ECS/Fargate の利用
  • Blue-Green デプロイメント
  • カナリアリリース
  • モニタリングとログ集約

参考リンク

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?