Stage 9: DevOps と CI/CD — デプロイの自動化
「動く」から「届ける」へ
「マイさん、最近デプロイが怖くて……毎回手でコマンドを叩いて、ミスしそうで。」
「それは設計の問題だ。デプロイは再現可能で、自動化されて、安全でなければならない。」
「DevOpsはツールではなく文化。開発(Development)と運用(Operations)の壁をなくして、コードを書いた人が安全にデプロイできるようにする考え方。CI/CDはそれを実現するパイプライン。」
CI/CDパイプラインの構造
Loading diagram...
GitHub Actionsで始めるCI
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: RuboCop
run: bundle exec rubocop --format github
- name: Brakeman
run: bundle exec brakeman --no-pager -w2
test:
name: Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_test
ports: ["5432:5432"]
options: --health-cmd pg_isready --health-interval 10s
redis:
image: redis:7-alpine
ports: ["6379:6379"]
env:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test
REDIS_URL: redis://localhost:6379/1
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Setup database
run: bundle exec rails db:schema:load
- name: Run RSpec
run: bundle exec rspec --format progress --format RspecJunitFormatter --out tmp/test-results.xml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: tmp/test-results.xml
build:
name: Build Docker Image
runs-on: ubuntu-latest
needs: [lint, test]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Login to ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Docker image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/myapp:$IMAGE_TAG .
docker push $ECR_REGISTRY/myapp:$IMAGE_TAG
echo "image=$ECR_REGISTRY/myapp:$IMAGE_TAG" >> $GITHUB_OUTPUTDockerfileのベストプラクティス
# Dockerfile
# ベースイメージ: 公式Rubyイメージ
FROM ruby:3.3-slim AS base
# セキュリティ: 非rootユーザーで実行
RUN groupadd -r app && useradd -r -g app app
WORKDIR /app
# 依存のインストール(キャッシュ活用)
FROM base AS dependencies
RUN apt-get update -qq && apt-get install -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY Gemfile Gemfile.lock ./
RUN bundle config set without "development test" \
&& bundle install --jobs 4 --retry 3
# 本番イメージ
FROM base AS production
COPY --from=dependencies /usr/local/bundle /usr/local/bundle
COPY --chown=app:app . .
# assets:precompile
ENV RAILS_ENV=production SECRET_KEY_BASE=dummy
RUN bundle exec rails assets:precompile
USER app
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]ECSへのデプロイ
# .github/workflows/deploy.yml
name: Deploy to ECS
on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]
jobs:
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
environment: staging
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Download task definition
run: |
aws ecs describe-task-definition \
--task-definition myapp-staging \
--query taskDefinition > task-definition.json
- name: Update ECS task definition with new image
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: app
image: ${{ secrets.ECR_REGISTRY }}/myapp:${{ github.sha }}
- name: Run DB migrations
run: |
aws ecs run-task \
--cluster myapp-staging \
--task-definition ${{ steps.task-def.outputs.task-definition }} \
--overrides '{"containerOverrides":[{"name":"app","command":["bundle","exec","rails","db:migrate"]}]}'
- name: Deploy to ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: myapp-staging
cluster: myapp-staging
wait-for-service-stability: true
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [deploy-staging]
environment: production # 承認が必要なgithub environment
steps:
# ステージングと同様の手順
- name: Deploy to Production ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: myapp-production
cluster: myapp-production
wait-for-service-stability: trueINFO
GitHub Environmentsの承認フロー
environment: production を設定し、GitHubリポジトリのSettings > Environmentsで承認者を設定すると、本番デプロイ前に手動承認ゲートが追加されます。
ブルーグリーンデプロイ
「ダウンタイムなしでデプロイするためのパターン。」
Loading diagram...
# ECSでのブルーグリーンデプロイ設定
# CodeDeployと組み合わせて実現
aws deploy create-deployment \
--application-name myapp \
--deployment-group-name myapp-prod \
--revision '{
"revisionType": "AppSpecContent",
"appSpecContent": {
"content": "{\"version\":1,\"Resources\":[{\"TargetService\":{\"Type\":\"AWS::ECS::Service\",\"Properties\":{\"TaskDefinition\":\"arn:aws:ecs:...\",\"LoadBalancerInfo\":{\"ContainerName\":\"app\",\"ContainerPort\":3000}}}}]}"
}
}'インフラ as Code (IaC)
# lib/tasks/infrastructure.rake
namespace :infra do
desc "Create RDS snapshot before migration"
task snapshot_before_migration: :environment do
rds = Aws::RDS::Client.new(region: "ap-northeast-1")
snapshot_id = "pre-migration-#{Time.current.strftime('%Y%m%d%H%M%S')}"
rds.create_db_snapshot(
db_instance_identifier: ENV.fetch("RDS_INSTANCE_ID"),
db_snapshot_identifier: snapshot_id
)
puts "スナップショット作成中: #{snapshot_id}"
rds.wait_until(:db_snapshot_available, db_snapshot_identifier: snapshot_id)
puts "スナップショット完了: #{snapshot_id}"
end
end# CloudFormation / ECSタスク定義
AWSTemplateFormatVersion: "2010-09-09"
Resources:
AppTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: myapp
NetworkMode: awsvpc
RequiresCompatibilities: [FARGATE]
Cpu: "512"
Memory: "1024"
ExecutionRoleArn: !GetAtt ECSExecutionRole.Arn
ContainerDefinitions:
- Name: app
Image: !Sub "${AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:latest"
PortMappings:
- ContainerPort: 3000
Environment:
- Name: RAILS_ENV
Value: production
Secrets:
- Name: SECRET_KEY_BASE
ValueFrom: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/myapp/prod/SECRET_KEY_BASE"
- Name: DATABASE_URL
ValueFrom: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/myapp/prod/DATABASE_URL"
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: /ecs/myapp
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: app
HealthCheck:
Command: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
Interval: 30
Timeout: 5
Retries: 3秘密情報の管理
# 環境変数は直接コードに書かない
# config/credentials.yml.enc を使う(Rails標準)
# または AWS Parameter Store / Secrets Manager
# AWS Parameter Storeからの読み込み
class SecretsManager
def self.fetch(key)
@cache ||= {}
@cache[key] ||= begin
ssm = Aws::SSM::Client.new(region: "ap-northeast-1")
ssm.get_parameter(name: key, with_decryption: true).parameter.value
end
end
end
# config/initializers/secrets.rb
if Rails.env.production?
ENV["STRIPE_API_KEY"] = SecretsManager.fetch("/myapp/prod/STRIPE_API_KEY")
ENV["SENDGRID_API_KEY"] = SecretsManager.fetch("/myapp/prod/SENDGRID_API_KEY")
endWARNING
絶対にやってはいけないこと
- APIキーやパスワードをコードに直書き
- .envファイルをGitにコミット
- 本番DBのパスワードをSlackに投稿
秘密情報は必ずAWS Parameter Store、Secrets Manager、またはGitHub Secretsで管理しましょう。
モニタリングとアラート
# config/initializers/cloudwatch.rb
# アプリのカスタムメトリクスをCloudWatchに送信
class MetricsPublisher
def self.record(namespace:, metric_name:, value:, unit: "Count")
return unless Rails.env.production?
Thread.new do
Aws::CloudWatch::Client.new(region: "ap-northeast-1").put_metric_data(
namespace: namespace,
metric_data: [{
metric_name: metric_name,
value: value,
unit: unit,
timestamp: Time.current
}]
)
rescue StandardError => e
Rails.logger.error("CloudWatch metric failed: #{e.message}")
end
end
end
# 使用例
MetricsPublisher.record(
namespace: "MyApp/Orders",
metric_name: "OrdersCreated",
value: 1
)Stage 9 のまとめ
「デプロイは恐怖であってはならない。ルーチンであるべきだ。」マイが言った。
「自動化されていれば、毎日デプロイできる。毎日デプロイできれば、変更が小さくなる。変更が小さければ、問題が起きても影響が少ない。これがDevOpsのサイクル。」
| 段階 | 目標 | ツール |
|---|---|---|
| CI(継続的インテグレーション) | 常にテストが通る状態 | GitHub Actions, RSpec |
| CD(継続的デリバリー) | 常にデプロイ可能な状態 | ECS, CodeDeploy |
| 監視 | 問題を素早く発見 | CloudWatch, DataDog |
| 秘密情報管理 | 認証情報を安全に | SSM Parameter Store |
「最後のステージは分散システム。ここまで来たヒロシならきっと理解できる。」
ヒロシはGitHub Actionsのワークフローを設定しながら、初めて「デプロイが楽しい」と感じた。