概要
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にします。
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を使ったりなど、アーキテクチャの幅が広がりそうです。