9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

CDKでWordpressをFargate+EFS環境に移行する

Last updated at Posted at 2020-06-23

概要

AWS ECS Fargate1.4.0がついにEFS(Elastic File System)をサポートしました。

これまではコンテナ内にボリュームをマウントしていたため、タスク数が増えたり、タスクがリフレッシュされるとマウントしていたデータが消えてしまっていたので、Wordpressのようなローカルにファイルを保持するようなアプリケーションにFargateは適用できませんでした。

EFSを利用することで、データを永続的に保持することが可能になるため、WordpressでもFargateを利用することができます。

今回は、CDKを利用してWordpressが稼働するインフラ環境を構築しました。

インフラ構築

1. セキュリティグループを定義

RDS、EC2、EFSのセキュリティグループを作成します。

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

    const vpc = ec2.Vpc.fromLookup(this, 'VpcForSystem', { vpcName: 'vpc' });

    // RDS用のセキュリティグループ
    // ポート3306番のTCP通信のみを許可
    const dbSecurityGroup = new ec2.CfnSecurityGroup(this, 'RDSSecurityGroup', {
      groupDescription: 'RDS Secutiry Group',
      tags: 'wordpress-rds-sg',
      securityGroupIngress: [
        {
          ipProtocol: 'tcp',
          cidrIp: '0.0.0.0/0',
          fromPort: 3306,
          toPort: 3306,
        }
      ],
      vpcId: vpc.vpcId,
    });

    // EC2用のセキュリティグループ
    // ポート22番のSSH通信のみを許可(踏み台用)
    const ec2SecurityGroup = new ec2.CfnSecurityGroup(this, 'EC2SecurityGroup', {
      groupDescription: 'EC2 Secutiry Group',
      securityGroupIngress: [
        {
          ipProtocol: 'tcp',
          cidrIp: '0.0.0.0/0',
          fromPort: 22,
          toPort: 22,
        }
      ],
      tags: 'wordpress-ec2-sg',
      vpcId: vpc.vpcId,
    });

    // EFS用のセキュリティグループ
    // ポート3306番のTCP通信のみを許可
    const efsSecurityGroup = new ec2.CfnSecurityGroup(this, 'EFSSecurityGroup', {
      groupDescription: 'EFS Secutiry Group',
      tags: 'wordpress-efs-sg',
      securityGroupIngress: [
        {
          ipProtocol: 'tcp',
          cidrIp: '0.0.0.0/0',
          fromPort: 2049,
          toPort: 2049,
        }
      ],
      vpcId: vpc.vpcId,
    });
  }
}

2. EC2の作成

ローカルからRDSやEFSにアクセスするための踏み台のインスタンスを作成します。

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

    const instance = new ec2.Instance(this, 'Instance', {
      instanceType: new ec2.InstanceType('t2.micro'),
      machineImage: ec2.MachineImage.latestAmazonLinux(),
      vpc,
      allowAllOutbound: true,
      instanceName: 'wordpress-bastion',
      keyName: 'wordpress-key',
      securityGroup: ec2.SecurityGroup.fromSecurityGroupId(this, 'Ec2SecurityGroupId', cdk.Fn.importValue('Ec2-SecurityGroup-GroupId')),
    });
    instance.addUserData(
      'yum -y update',
      'rpm -Uvh http://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm',
      'yum -y install --enablerepo=mysql57-community mysql-community-client php-mysqlnd',
      'yum -y install amazon-efs-utils',
      `mkdir -p /mnt/efs/${FileSystemId}`,
      `mount -t efs ${cdk.Fn.importValue(FileSystemId}:/ /mnt/efs/${FileSystemId}`
    );
  }
}

3. RDSの作成

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

    // Parameter Group
    const parameterGroup = new rds.ParameterGroup(this, 'ParameterGroup', {
      description: 'DB Parameter Group',
      family: 'mysql5.7',
      parameters: {
        'character_set_connection': 'utf8',
        'character_set_database': 'utf8',
        'character_set_results': 'utf8',
        'character_set_server': 'utf8',
        'character_set_client': 'utf8',
      },
    });

    // Instance
    const databaseInstance = new rds.DatabaseInstance(this, 'DbInstance', {
      allocatedStorage: 20,
      autoMinorVersionUpgrade: false,
      backupRetention: cdk.Duration.days(7),
      cloudwatchLogsRetention: logs.RetentionDays.TWO_MONTHS,
      copyTagsToSnapshot: true,
      engine: rds.DatabaseInstanceEngine.MYSQL,
      engineVersion: '5.7',
      instanceClass: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
      instanceIdentifier: 'wordpress-rds',
      licenseModel: rds.LicenseModel.GENERAL_PUBLIC_LICENSE,
      masterUsername: 'wp_admin',
      masterUserPassword: this.node.tryGetContext('rds-password'),
      multiAz: false,
      parameterGroup,
      securityGroups: [
        ec2.SecurityGroup.fromSecurityGroupId(this, 'DbSecurityGroup', cdk.Fn.importValue(DBSecurityGroupId)),
      ],
      storageEncrypted: false,
      storageType: rds.StorageType.GP2,
      enablePerformanceInsights: false,
      vpc,
    });
  }
}

4. ALBの作成

export class LbStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, stackUtil: StackUtil, externalProps: IExternalProperties, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC
    const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { vpcName: stackUtil.getVpcName() });

    // Security Group for LB
    const lbSecurityGroup = new ec2.SecurityGroup(this, 'LbSecurityGroup', {
      securityGroupName: stackUtil.getName('LB-SG'),
      description: 'Security group for Loadbalancer.',
      vpc,
      allowAllOutbound: true,
    });
    lbSecurityGroup.addIngressRule(ec2.Peer.ipv4('0.0.0.0/0'), ec2.Port.tcp(80), '');
    lbSecurityGroup.addIngressRule(ec2.Peer.ipv4('0.0.0.0/0'), ec2.Port.tcp(443), '');
    stackUtil.addTagToResource(lbSecurityGroup, 'LB-SG');

    // Application Load Balancer
    const alb = new elbv2.CfnLoadBalancer(this, 'LoadBalancer', {
      name: 'wordpress-alb',
      type: 'application',
      scheme: 'internet-facing',
      subnets: vpc.publicSubnets.map(c => c.subnetId),
      securityGroups: [
        lbSecurityGroup.securityGroupId,
      ],
    });

    // Target Group for ALB
    const targetGroup = new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {
      deregistrationDelay: cdk.Duration.seconds(15),
      healthCheck: {
        healthyThresholdCount: 3,
        healthyHttpCodes: '200-299,301,302,303,304',
        interval: cdk.Duration.seconds(90),
        path: '/',
        protocol: elbv2.Protocol.HTTP,
        timeout: cdk.Duration.seconds(60),
        unhealthyThresholdCount: 3,
      },
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP,
      targetGroupName: 'wordpress-tg',
      targetType: elbv2.TargetType.IP,
      vpc,
    });

    // HTTPリクエストの場合は、HTTPSにリダイレクト
    new elbv2.CfnListener(this, 'ListenerHttp', {
      port: 80,
      protocol: elbv2.Protocol.HTTP,
      loadBalancerArn: alb.ref,
      defaultActions: [
        { type: 'redirect', redirectConfig: { port: '443', protocol: elbv2.Protocol.HTTPS, statusCode: 'HTTP_301' } },
      ],
    });

    new elbv2.CfnListener(this, 'ListenerHttps', {
      port: 443,
      protocol: elbv2.Protocol.HTTPS,
      loadBalancerArn: alb.ref,
      certificates: [
        { certificateArn: '[LBの証明書ARN]', },
      ],
      defaultActions: [
        { targetGroupArn: targetGroup.targetGroupArn, type: 'forward', },
      ],
    });
  }
}

5. EFSの作成

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

    const fileSystem = new efs.FileSystem(this, 'FileSystem', {
      vpc,
      fileSystemName: 'wordpress-efs',
      performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
      securityGroup: ec2.SecurityGroup.fromSecurityGroupId(this, 'EfsSecurityGroup', cdk.Fn.importValue('EfsSecurityGroupd')),
    });
  }
}

6. System Managerのパラメータストアにパラメータ定義

Fargateのコンテナから環境変数を参照するために、System Managerのパラメータストアにパラメータを定義しておきます。

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

    new ssm.StringParameter(this, 'WpDbHostParameter', {
      description: 'Wordpress DB Host',
      parameterName: '/wordpress/db_host',
      stringValue: '[RDSのDBエンドポイント]',
    });

    new ssm.StringParameter(this, 'WpDbPortParameter', {
      description: 'Wordpress DB Port',
      parameterName: '/wordpress/db_port',
      stringValue: '[RDSのDBポート]',
    });

    new ssm.StringParameter(this, 'WpDbPasswordParameter', {
      description: 'Wordpress DB Password',
      parameterName: '/wordpress/db_password',
      stringValue: 'password',
    });

    new ssm.StringParameter(this, 'WpDbNameParameter', {
      description: 'Wordpress DB Name',
      parameterName: '/wordpress/db_name',
      stringValue: 'wordpress',
    });

    new ssm.StringParameter(this, 'WpDbUserParameter', {
      description: 'Wordpress DB User',
      parameterName: '/wordpress/db_user',
      stringValue: 'wp_admin',
    });
  }
}

7. ECSの作成

ECSのリソースを作成します。
注意すべき点は、2020年6月現在Fargateのlatestバージョンは1.3.0ですが、1.3.0ではEFSをサポートしていないため、1.4.0を指定する必要があります。
また、CDK及びCloudformationではまだ、インフラコードからFargateとEFSの連携ができないため、手で作成する必要があります。
タスク定義をインフラコードで作成した後、コンソールから「新しいリビジョン」を選択し、EFSの追加、コンテナ上でEFSをマウントします。
コンテナパスは/var/www/htmlにします。

スクリーンショット 2020-06-23 16.21.07.png スクリーンショット 2020-06-23 16.22.14.png
export class EcsStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, stackUtil: StackUtil, props?: cdk.StackProps) {
    super(scope, id, props);

    // Load ECS Cluster
    const cluster = new ecs.CfnCluster(this, 'Cluster', {
      clusterName: 'wordpress-cluster',
    });

    // Task Execution Role
    const executionRole = new iam.Role(this, 'TaskExecutionRole', {
      roleName: 'wordpress-taskexecution-role',
      assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'),
      ],
      inlinePolicies: {
        'INLINE': new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'ssm:GetParameter',
                'ssm:GetParameters',
                'ssm:GetParameterHistory',
                'ssm:GetParametersByPath',
              ],
              resources: [
                `arn:aws:ssm:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:parameter/*`,
              ],
            }),
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'ssm:DescribeParameters',
              ],
              resources: [
                '*',
              ],
            }),
          ],
        }),
      },
    });

    // Task Role
    const taskRole = new iam.Role(this, 'TaskRole', {
      roleName: 'wordpress-task-role',
      assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
      inlinePolicies: {
        'INLINE': new iam.PolicyDocument({
          statements: [                 
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'ssm:GetParameter',
                'ssm:GetParameters',
                'ssm:GetParameterHistory',
                'ssm:GetParametersByPath',
              ],
              resources: [
                `arn:aws:ssm:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:parameter/*`,
              ],
            }),
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                'ssm:DescribeParameters',
              ],
              resources: [
                '*',
              ],
            }),
          ],
        }),
      },
    });

    // Task Definition
    let taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {
      cpu: 256,
      executionRole,
      family: 'wordpress-taskdefinition',
      memoryLimitMiB: 512,
      taskRole,
    });

    // Container
    let containerDefinition = new ecs.ContainerDefinition(this, 'wordpress', {
      image: ecs.ContainerImage.fromRegistry('wordpress:latest'),
      taskDefinition,
      cpu: 256,
      environment: {
        REGION: cdk.Aws.REGION,
      },
      logging: ecs.LogDriver.awsLogs({
        streamPrefix: 'wordpress',
        logGroup,
      }),
      memoryReservationMiB: 512,
      secrets: {
        WORDPRESS_DB_HOST: ecs.Secret.fromSsmParameter(
          ssm.StringParameter.fromStringParameterName(this, 'WpDbHostParameterName', `/wordpress/db_host`),
        ),
        WORDPRESS_DB_PORT: ecs.Secret.fromSsmParameter(
          ssm.StringParameter.fromStringParameterName(this, 'WpDbPortParameterName', `/wordpress/db_port`),
        ),
        WORDPRESS_DB_PASSWORD: ecs.Secret.fromSsmParameter(
          ssm.StringParameter.fromStringParameterName(this, 'WpDbPasswordParameterName', `/wordpress/db_password`),
        ),
        WORDPRESS_DB_NAME: ecs.Secret.fromSsmParameter(
          ssm.StringParameter.fromStringParameterName(this, 'WpDbNameParameterName', `/wordpress/db_name`),
        ),
        WORDPRESS_DB_USER: ecs.Secret.fromSsmParameter(
          ssm.StringParameter.fromStringParameterName(this, 'WpDbUserParameterName', `/wordpress/db_user`),
        ),
      }
    });
    containerDefinition.addPortMappings({
      containerPort: 80,
      hostPort: 80,
      protocol: ecs.Protocol.TCP,
    });

    // Service
    const fargateService = new ecs.CfnService(this, 'EcsService', {
      serviceName: 'wordpress-service',
      cluster: cluster.clusterArn,
      desiredCount: 1,
      launchType: ecs.LaunchType.FARGATE,
      loadBalancers: [
        {
          containerName: 'wordpress'
          containerPort: 80,
          targetGroupArn: cdk.Fn.importValue('LbTgArn'),
        },
      ],
      deploymentConfiguration: {
        maximumPercent: 200,
        minimumHealthyPercent: 100,
      },
      networkConfiguration: {
        awsvpcConfiguration: {
          securityGroups: [
            systemSecurityGroup.securityGroupId,
          ],
          subnets: vpc.publicSubnets.map(c => c.subnetId),
          assignPublicIp: 'ENABLED',
        },
      },
      platformVersion: '1.4.0', // TODO: latest
      healthCheckGracePeriodSeconds: 30,
      taskDefinition: taskDefinition.taskDefinitionArn,
    });
  }
}

まとめ

Fargateを利用することでログの可視化(Cloudwatch)や、セキュリティ強化のためにWAFを導入したり、死活監視でXrayを使ったりなど、アーキテクチャの幅が広がりそうです。

9
4
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
9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?