AWS アプリケーションデプロイガイド

AWSEC2ECSLambdaCI/CDCDKCloudFormationDevOps

概要

本ガイドでは、AWS上でアプリケーションをデプロイする際のベストプラクティスを解説します。EC2、ECS、Lambda、そしてCI/CDパイプラインを使用した、スケーラブルで信頼性の高いデプロイメント戦略を紹介します。Infrastructure as Code(IaC)にはAWS CDKとCloudFormationを活用し、負荷分散、自動スケーリング、セキュリティ、コスト最適化についても詳しく説明します。

詳細

EC2デプロイメントパターン

EC2インスタンスは、完全なOS制御が必要な場合や、特定のハードウェア構成が必要なアプリケーションに最適です。AWS CDKを使用することで、インフラストラクチャをコードとして管理し、再現可能なデプロイメントを実現できます。

VPC構成のベストプラクティス

  • デフォルトセキュリティグループのインバウンド/アウトバウンドトラフィックを制限
  • プライベートサブネットとパブリックサブネットの適切な分離
  • NATゲートウェイまたはNATインスタンスによるアウトバウンド接続の管理

インスタンス構成

  • CloudFormation Init(cfn-init)を使用した初期設定の自動化
  • ユーザーデータスクリプトによるソフトウェアのインストールとサービスの起動
  • Auto Scaling Groupによる可用性の確保

ECS/Fargateコンテナデプロイメント

ECSは、Dockerコンテナのオーケストレーションを完全管理するサービスです。Fargateを使用することで、サーバーレスコンテナの実行が可能になります。

起動タイプの選択

  • EC2起動タイプ: インスタンスとタスク配置を完全制御
  • Fargate起動タイプ: AWSがインフラを管理、サーバーレス実行
  • ECS Anywhere: オンプレミスインフラでのタスク実行

タスク定義の最適化

  • 軽量なベースイメージの使用
  • コンテナイメージレイヤーの最小化
  • 適切なCPUとメモリの割り当て(Fargateは1/4 vCPU〜16 vCPU、512MB〜120GBメモリをサポート)

自動スケーリング設定

  • Application Auto Scalingでのターゲット追跡
  • スケジュールベースのスケーリング
  • カスタムメトリクスベースのスケーリング

Lambdaサーバーレスデプロイメント

Lambdaは、イベント駆動型アーキテクチャと短時間実行タスクに最適です。2025年のベストプラクティスでは、コールドスタートの最適化が重要です。

コールドスタート最適化

  • パッケージサイズの最小化(必要なSDKコンポーネントのみインポート)
  • 最適なランタイムの選択(Python、Node.js、Goは500ms未満のコールドスタートを実現)
  • Provisioned Concurrencyによる事前初期化
  • メモリ構成の最適化(AWS Lambda Power Tuningツールの活用)

コード初期化の最適化

  • ステートレス関数の設計
  • 不要な外部依存関係の削除
  • 静的変数の最小化

CI/CDパイプライン構築

AWS CodePipelineとGitHub Actionsを組み合わせた、モダンなCI/CDパイプラインの構築方法を解説します。

ネイティブAWS CI/CDツール

  • AWS CodePipeline: パイプラインのオーケストレーション
  • AWS CodeBuild: ビルドとテストの実行
  • AWS CodeDeploy: EC2、ECS、Lambda、オンプレミスへのデプロイメント自動化

セキュアな認証

  • IAMロールとOIDCによる認証
  • AWS Secrets ManagerまたはParameter Storeでのシークレット管理
  • GitHub ActionsとAWS間の信頼関係確立

デプロイメント戦略

  • Blue/Greenデプロイメント
  • カナリアデプロイメント(段階的ロールアウト)
  • ローリングデプロイメント
  • イミュータブルデプロイメント

Infrastructure as Code(CDK/CloudFormation)

AWS CDKを使用した、タイプセーフで再利用可能なインフラストラクチャの定義方法を説明します。

CDKベストプラクティス

  • コンストラクトの再利用性を重視した設計
  • 環境ごとの設定管理(開発、ステージング、本番)
  • スタック間の依存関係の適切な管理
  • Feature Flagsによる段階的な機能導入

CloudFormationテンプレート

  • テンプレートの分割と再利用
  • パラメータとマッピングの活用
  • クロススタック参照の使用
  • ドリフト検出によるインフラの一貫性確保

ロードバランシングと自動スケーリング

高可用性と弾力性を実現するための設定方法を解説します。

ロードバランサーの選択

  • Application Load Balancer(ALB): HTTP/HTTPSトラフィック、パスベースルーティング
  • Network Load Balancer(NLB): TCP/UDPトラフィック、超低レイテンシ
  • Classic Load Balancer: レガシーアプリケーション(新規使用は非推奨)

Auto Scalingの設定

  • ターゲット追跡スケーリングポリシー
  • ステップスケーリングポリシー
  • 予測スケーリング
  • スケジュールベースのスケーリング

可用性ゾーンの分散

  • 最低3つのAZへの分散
  • クロスゾーン負荷分散の有効化
  • ヘルスチェックの適切な設定

セキュリティベストプラクティス

アプリケーションとインフラストラクチャのセキュリティを確保する方法を説明します。

ネットワークセキュリティ

  • セキュリティグループによる最小権限の原則
  • NACLによる追加のネットワーク層保護
  • VPCエンドポイントによるプライベート接続
  • AWS WAFによるWebアプリケーション保護

アクセス管理

  • IAMロールとポリシーの最小権限設定
  • 一時的な認証情報の使用
  • MFAの強制
  • CloudTrailによる監査ログの記録

データ保護

  • 保存時の暗号化(EBS、S3、RDS)
  • 転送時の暗号化(TLS/SSL)
  • AWS KMSによるキー管理
  • バックアップとディザスタリカバリ計画

コスト最適化戦略

効率的なリソース使用とコスト削減の方法を解説します。

インスタンスの最適化

  • 適切なインスタンスタイプの選択
  • リザーブドインスタンスとSavings Plansの活用
  • スポットインスタンスの戦略的使用
  • 不要なリソースの自動停止

リソースの効率化

  • オンデマンドスケーリングによる無駄の削減
  • キャッシングによるコンピュート負荷の軽減
  • データ転送コストの最適化
  • ライフサイクルポリシーによる古いデータの管理

モニタリングと最適化

  • AWS Cost Explorerによるコスト分析
  • Trusted Advisorによる推奨事項の確認
  • タグ付けによるコスト配分
  • Budget Alertsによるコスト超過の防止

メリット・デメリット

メリット

  1. スケーラビリティ: 需要に応じた自動的なリソース調整
  2. 高可用性: マルチAZ構成による障害耐性
  3. 自動化: CI/CDパイプラインによる迅速なデプロイメント
  4. セキュリティ: AWSのセキュリティベストプラクティスの活用
  5. コスト効率: 使用した分だけの支払い、最適化オプション
  6. マネージドサービス: 運用負荷の軽減
  7. グローバル展開: 世界中のリージョンへの簡単なデプロイ

デメリット

  1. 学習曲線: AWSサービスの複雑性による初期学習コスト
  2. ベンダーロックイン: AWS固有サービスへの依存
  3. コスト管理: 不適切な設定による予期しないコスト増加
  4. ネットワークレイテンシ: リージョン間通信の遅延
  5. 複雑性: マイクロサービスアーキテクチャの管理オーバーヘッド

参考ページ

書き方の例

EC2へのアプリケーションデプロイ(CDK)

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as autoscaling from 'aws-cdk-lib/aws-autoscaling';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';

export class EC2AppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPCの作成
    const vpc = new ec2.Vpc(this, 'AppVPC', {
      maxAzs: 3,
      natGateways: 2,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
    });

    // セキュリティグループ
    const webSg = new ec2.SecurityGroup(this, 'WebSecurityGroup', {
      vpc,
      description: 'Security group for web servers',
      allowAllOutbound: true,
    });
    webSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP');
    webSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'Allow HTTPS');

    // IAMロール
    const role = new iam.Role(this, 'InstanceRole', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
      ],
    });

    // 起動テンプレート
    const launchTemplate = new ec2.LaunchTemplate(this, 'LaunchTemplate', {
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
      machineImage: ec2.MachineImage.latestAmazonLinux2(),
      securityGroup: webSg,
      role: role,
      userData: ec2.UserData.forLinux(),
    });

    // ユーザーデータスクリプト
    launchTemplate.userData?.addCommands(
      'yum update -y',
      'yum install -y httpd',
      'systemctl start httpd',
      'systemctl enable httpd',
      'echo "<h1>Hello from AWS EC2</h1>" > /var/www/html/index.html'
    );

    // Auto Scaling Group
    const asg = new autoscaling.AutoScalingGroup(this, 'ASG', {
      vpc,
      launchTemplate,
      minCapacity: 2,
      maxCapacity: 10,
      desiredCapacity: 3,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
      },
      healthCheck: autoscaling.HealthCheck.elb({
        grace: cdk.Duration.seconds(300),
      }),
    });

    // Application Load Balancer
    const alb = new elbv2.ApplicationLoadBalancer(this, 'ALB', {
      vpc,
      internetFacing: true,
      securityGroup: webSg,
    });

    const listener = alb.addListener('Listener', {
      port: 80,
      open: true,
    });

    listener.addTargets('Target', {
      port: 80,
      targets: [asg],
      healthCheck: {
        path: '/',
        interval: cdk.Duration.seconds(30),
      },
    });

    // Auto Scalingポリシー
    asg.scaleOnCpuUtilization('CpuScaling', {
      targetUtilizationPercent: 70,
    });

    asg.scaleOnRequestCount('RequestScaling', {
      targetRequestsPerMinute: 1000,
    });

    // 出力
    new cdk.CfnOutput(this, 'LoadBalancerDNS', {
      value: alb.loadBalancerDnsName,
      description: 'Load balancer DNS name',
    });
  }
}

ECS Fargateサービスのデプロイ(CDK)

import * as cdk from 'aws-cdk-lib';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';

export class EcsFargateStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ECSクラスターの作成
    const cluster = new ecs.Cluster(this, 'Cluster', {
      clusterName: 'app-cluster',
      containerInsights: true,
    });

    // Fargateサービスの作成(ALB付き)
    const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Service', {
      cluster,
      cpu: 512,
      memoryLimitMiB: 1024,
      desiredCount: 3,
      taskImageOptions: {
        image: ecs.ContainerImage.fromRegistry('nginx:latest'),
        containerPort: 80,
        environment: {
          ENVIRONMENT: 'production',
        },
      },
      publicLoadBalancer: true,
      domainName: 'app.example.com',
      domainZone: route53.HostedZone.fromLookup(this, 'Zone', {
        domainName: 'example.com',
      }),
      certificate: certificatemanager.Certificate.fromCertificateArn(
        this,
        'Certificate',
        'arn:aws:acm:region:account:certificate/certificate-id'
      ),
    });

    // Auto Scaling設定
    const scaling = fargateService.service.autoScaleTaskCount({
      minCapacity: 2,
      maxCapacity: 20,
    });

    scaling.scaleOnCpuUtilization('CpuScaling', {
      targetUtilizationPercent: 50,
    });

    scaling.scaleOnMemoryUtilization('MemoryScaling', {
      targetUtilizationPercent: 70,
    });

    // スケジュールベースのスケーリング
    scaling.scaleOnSchedule('BusinessHours', {
      schedule: autoscaling.Schedule.cron({
        hour: '8',
        minute: '0',
        weekDay: '1-5',
      }),
      minCapacity: 10,
      maxCapacity: 20,
    });

    scaling.scaleOnSchedule('OffHours', {
      schedule: autoscaling.Schedule.cron({
        hour: '20',
        minute: '0',
        weekDay: '1-5',
      }),
      minCapacity: 2,
      maxCapacity: 5,
    });

    // CloudWatchアラーム
    const cpuAlarm = fargateService.service.metricCpuUtilization().createAlarm(this, 'CpuAlarm', {
      threshold: 80,
      evaluationPeriods: 3,
      datapointsToAlarm: 2,
    });

    // 出力
    new cdk.CfnOutput(this, 'ServiceURL', {
      value: `https://${fargateService.loadBalancer.loadBalancerDnsName}`,
    });
  }
}

Lambda関数のデプロイ(CDK)

import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';

export class LambdaApiStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda関数の作成(コールドスタート最適化)
    const apiFunction = new lambda.Function(this, 'ApiFunction', {
      runtime: lambda.Runtime.PYTHON_3_11, // 高速な起動時間
      code: lambda.Code.fromAsset('lambda'),
      handler: 'app.handler',
      memorySize: 1024, // 最適化されたメモリサイズ
      timeout: cdk.Duration.seconds(30),
      environment: {
        ENVIRONMENT: 'production',
      },
      architecture: lambda.Architecture.ARM_64, // コスト効率の良いアーキテクチャ
      tracing: lambda.Tracing.ACTIVE,
    });

    // Provisioned Concurrency(本番環境用)
    const alias = new lambda.Alias(this, 'ProdAlias', {
      aliasName: 'prod',
      version: apiFunction.currentVersion,
      provisionedConcurrentExecutions: 5, // 常時5つの初期化済み環境
    });

    // API Gatewayの作成
    const api = new apigateway.RestApi(this, 'Api', {
      restApiName: 'service-api',
      deployOptions: {
        stageName: 'prod',
        throttlingBurstLimit: 5000,
        throttlingRateLimit: 1000,
        tracingEnabled: true,
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
      },
    });

    // APIエンドポイントの設定
    const integration = new apigateway.LambdaIntegration(alias, {
      requestTemplates: { 'application/json': '{ "statusCode": "200" }' },
    });

    api.root.addMethod('GET', integration);
    
    const items = api.root.addResource('items');
    items.addMethod('GET', integration);
    items.addMethod('POST', integration);
    
    const item = items.addResource('{id}');
    item.addMethod('GET', integration);
    item.addMethod('PUT', integration);
    item.addMethod('DELETE', integration);

    // CloudWatchダッシュボード
    const dashboard = new cloudwatch.Dashboard(this, 'ApiDashboard', {
      dashboardName: 'lambda-api-dashboard',
    });

    dashboard.addWidgets(
      new cloudwatch.GraphWidget({
        title: 'Lambda Performance',
        left: [apiFunction.metricInvocations()],
        right: [apiFunction.metricDuration()],
      }),
      new cloudwatch.GraphWidget({
        title: 'Lambda Errors',
        left: [apiFunction.metricErrors()],
        right: [apiFunction.metricThrottles()],
      }),
    );

    // アラーム設定
    new cloudwatch.Alarm(this, 'ErrorAlarm', {
      metric: apiFunction.metricErrors(),
      threshold: 10,
      evaluationPeriods: 2,
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    // 出力
    new cdk.CfnOutput(this, 'ApiUrl', {
      value: api.url,
      description: 'API Gateway URL',
    });
  }
}

CI/CDパイプライン(GitHub Actions + CDK)

import * as cdk from 'aws-cdk-lib';
import * as pipelines from 'aws-cdk-lib/pipelines';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';

export class PipelineStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // CodePipelineの作成
    const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
      pipelineName: 'AppDeploymentPipeline',
      synth: new pipelines.ShellStep('Synth', {
        input: pipelines.CodePipelineSource.connection('org/repo', 'main', {
          connectionArn: 'arn:aws:codestar-connections:region:account:connection/id',
        }),
        commands: [
          'npm ci',
          'npm run build',
          'npm run test',
          'npx cdk synth',
        ],
        primaryOutputDirectory: 'cdk.out',
      }),
      
      // CodeBuildのデフォルト設定
      codeBuildDefaults: {
        buildEnvironment: {
          computeType: codebuild.ComputeType.MEDIUM,
          privileged: true,
        },
        partialBuildSpec: codebuild.BuildSpec.fromObject({
          version: '0.2',
          phases: {
            install: {
              commands: [
                'n stable',
                'npm install -g aws-cdk',
              ],
            },
          },
          cache: {
            paths: ['node_modules/**/*'],
          },
        }),
      },
    });

    // 開発環境ステージ
    pipeline.addStage(new AppStage(this, 'Dev', {
      env: { account: '111111111111', region: 'us-east-1' },
    }), {
      pre: [
        new pipelines.ShellStep('Lint', {
          commands: ['npm run lint'],
        }),
      ],
      post: [
        new pipelines.ShellStep('IntegrationTest', {
          commands: [
            'npm run integration-test',
          ],
          envFromCfnOutputs: {
            ENDPOINT_URL: devStage.urlOutput,
          },
        }),
      ],
    });

    // 本番環境ステージ(承認付き)
    pipeline.addStage(new AppStage(this, 'Prod', {
      env: { account: '222222222222', region: 'us-east-1' },
    }), {
      pre: [
        new pipelines.ManualApprovalStep('PromoteToProd'),
      ],
    });
  }
}

// アプリケーションステージ
class AppStage extends cdk.Stage {
  public readonly urlOutput: cdk.CfnOutput;
  
  constructor(scope: Construct, id: string, props?: cdk.StageProps) {
    super(scope, id, props);

    const service = new EcsFargateStack(this, 'Service');
    this.urlOutput = service.urlOutput;
  }
}

Infrastructure as Codeベストプラクティス

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

// 再利用可能なコンストラクト
export class NetworkingConstruct extends Construct {
  public readonly vpc: ec2.Vpc;
  
  constructor(scope: Construct, id: string, props: NetworkingProps) {
    super(scope, id);

    this.vpc = new ec2.Vpc(this, 'VPC', {
      maxAzs: props.maxAzs || 3,
      natGateways: props.natGateways || 2,
      enableDnsHostnames: true,
      enableDnsSupport: true,
      subnetConfiguration: [
        {
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
          cidrMask: 24,
        },
        {
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
          cidrMask: 24,
        },
        {
          name: 'Isolated',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
          cidrMask: 24,
        },
      ],
    });

    // VPCエンドポイント
    this.vpc.addGatewayEndpoint('S3Endpoint', {
      service: ec2.GatewayVpcEndpointAwsService.S3,
    });

    this.vpc.addInterfaceEndpoint('ECREndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.ECR,
    });

    // タグ付け
    cdk.Tags.of(this.vpc).add('Environment', props.environment);
    cdk.Tags.of(this.vpc).add('Project', props.projectName);
  }
}

// 環境別設定
export class EnvironmentConfig {
  static getConfig(environment: string): AppConfig {
    const configs: { [key: string]: AppConfig } = {
      dev: {
        instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
        minCapacity: 1,
        maxCapacity: 3,
        alarmThreshold: 90,
        enableLogging: true,
      },
      staging: {
        instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.SMALL),
        minCapacity: 2,
        maxCapacity: 10,
        alarmThreshold: 80,
        enableLogging: true,
      },
      prod: {
        instanceType: ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE),
        minCapacity: 3,
        maxCapacity: 50,
        alarmThreshold: 70,
        enableLogging: true,
      },
    };

    return configs[environment] || configs.dev;
  }
}

// Feature Flagsの使用
export class FeatureFlags {
  static isEnabled(feature: string): boolean {
    const flags = {
      NEW_DEPLOYMENT_STRATEGY: process.env.ENABLE_NEW_DEPLOYMENT === 'true',
      ENHANCED_MONITORING: process.env.ENABLE_ENHANCED_MONITORING === 'true',
      CANARY_DEPLOYMENTS: process.env.ENABLE_CANARY === 'true',
    };

    return flags[feature] || false;
  }
}

セキュリティとコスト最適化

// セキュリティグループの最小権限設定
const dbSecurityGroup = new ec2.SecurityGroup(this, 'DatabaseSG', {
  vpc,
  description: 'Security group for RDS database',
  allowAllOutbound: false, // アウトバウンドも制限
});

// アプリケーションサーバーからのみアクセス許可
dbSecurityGroup.addIngressRule(
  appSecurityGroup,
  ec2.Port.tcp(3306),
  'Allow MySQL access from app servers only'
);

// Secrets Managerによるデータベース認証情報管理
const dbSecret = new secretsmanager.Secret(this, 'DBSecret', {
  description: 'RDS database credentials',
  generateSecretString: {
    secretStringTemplate: JSON.stringify({ username: 'admin' }),
    generateStringKey: 'password',
    excludeCharacters: ' %+~`#$&*()|[]{}:;<>?!\'/@"\\',
  },
});

// コスト最適化: スポットインスタンスの使用
const spotFleet = new ec2.CfnSpotFleet(this, 'SpotFleet', {
  spotFleetRequestConfigData: {
    iamFleetRole: spotFleetRole.roleArn,
    allocationStrategy: 'lowestPrice',
    targetCapacity: 10,
    launchSpecifications: [
      {
        instanceType: 't3.medium',
        imageId: ec2.MachineImage.latestAmazonLinux2().getImage(this).imageId,
        spotPrice: '0.05', // 最大入札価格
        securityGroups: [{ groupId: webSg.securityGroupId }],
      },
    ],
  },
});

// ライフサイクルポリシーによるコスト削減
const lifecycleRule = new s3.LifecycleRule({
  id: 'delete-old-logs',
  enabled: true,
  transitions: [
    {
      storageClass: s3.StorageClass.INFREQUENT_ACCESS,
      transitionAfter: cdk.Duration.days(30),
    },
    {
      storageClass: s3.StorageClass.GLACIER,
      transitionAfter: cdk.Duration.days(90),
    },
  ],
  expiration: cdk.Duration.days(365),
});