Amazon FSX for Windows Overview#
Keywords: AWS, Amazon, FSX, Overview.
Overview#
在 1990 - 2010 之间的很多老牌公司的内部电脑都是用 Windows File Server (WFS) 用来分享文件的. 简单来说 WFS 就像是一个内网的文件服务器大网盘, 然后个人电脑用客户端将盘挂载为虚拟硬盘, 对文件进行读写. 而配置 WFS 需要请人安装电脑, 配置网络, 可不是一件轻松的事情. 虽然已经进入了云时代, 但是由于 WFS 已经是公司内部成熟的工具, 并且很多人天天用, 所以不可能说一下子就切换成 S3 之类的后台存储, 还是要使用 WFS. 但企业又想减少运维和维护成本, 所以 AWS 推出了 FSx for WFS, 用来替代传统的 WFS. 你只需要将文件迁徙到 FSx 上, 然后大家的客户端重新挂载一下即可, 大大方便了 WFS 的维护工作.
Setup Amazon FSx for WFS and CloudFormation Stack#
Accessing SMB file shares remotely with Amazon FSx for Windows File Server 是一篇交你怎么使用 FSx for WFS 的官方博客, 建议精读. 但是整个步骤其实还是挺复杂的, 你需要创建很多 VPC, Active Directory, VPN Endpoint 等很多资源. 下面是我用 cottonformation 创建这些资源的脚本, 虽然其中有一些例如导入 Certificate 证书等步骤还是需要手动做, 但是这样已经自动化了 90% 的工作了.
1# -*- coding: utf-8 -*-
2
3from typing import List
4import attr
5from attrs_mate import AttrsClass
6import cottonformation as cf
7from cottonformation.res import ec2, directoryservice
8
9
10@attr.s
11class VpcStack(cf.Stack):
12 """
13 The Network infrastructure.
14 """
15 project_name: str = AttrsClass.ib_str(nullable=False)
16 stage: str = AttrsClass.ib_str(nullable=False)
17 vpc_cidr_seed: int = AttrsClass.ib_int(nullable=False)
18 n_az_used: int = AttrsClass.ib_int(nullable=False)
19 n_subnet_per_az_per_public_private: int = AttrsClass.ib_int(nullable=False)
20 sg_authorized_ips: List[str] = AttrsClass.ib_list_of_str()
21
22 active_directory_admin_password: str = AttrsClass.ib_str(nullable=False)
23 server_certificate_arn: str = AttrsClass.ib_str(nullable=False)
24 @property
25 def project_name_slug(self) -> str:
26 return self.project_name.replace("_", "-")
27
28 @property
29 def env_name(self):
30 return f"{self.project_name_slug}-{self.stage}"
31
32 @property
33 def stack_name(self) -> str:
34 return f"{self.env_name}-vpc"
35
36 @property
37 def vpc_cidr_block(self):
38 return f"10.{self.vpc_cidr_seed}.0.0/16"
39
40 @property
41 def public_subnet_cidr_block_list(self):
42 return [
43 "10.{}.{}.0/24".format(
44 self.vpc_cidr_seed,
45 ind * 2,
46 )
47 for ind in range(1, self.n_az_used + 1)
48 ]
49
50 def post_hook(self):
51 self.mk_rg1_vpc()
52 self.mk_rg2_subnet()
53 self.mk_rg3_route()
54 self.mk_pk4_security_group()
55 self.mk_rg5_active_directory()
56 self.mk_rg6_vpn_endpoint()
57
58 def mk_rg1_vpc(self):
59 self.rg1_vpc = cf.ResourceGroup("rg1_vpc")
60 self.vpc = ec2.VPC(
61 "VPC",
62 rp_CidrBlock=self.vpc_cidr_block,
63 p_EnableDnsHostnames=True,
64 p_Tags=cf.Tag.make_many(
65 Name=cf.Sub.from_params(f"{self.env_name}-vpc"),
66 Description=cf.Sub.from_params(f"The main vpc for {self.env_name}"),
67 ),
68 )
69 self.rg1_vpc.add(self.vpc)
70
71 self.out_vpc_id = cf.Output(
72 "VpcId",
73 Description="VPC Id",
74 Value=self.vpc.ref(),
75 Export=cf.Export(f"{self.env_name}-vpc-id"),
76 DependsOn=self.vpc,
77 )
78 self.rg1_vpc.add(self.out_vpc_id)
79
80 self.out_vpc_cidr_block = cf.Output(
81 "VpcCidrBlock",
82 Description="VPC Cidr Block",
83 Value=self.vpc.rv_CidrBlock,
84 Export=cf.Export(f"{self.env_name}-vpc-cidr-block"),
85 DependsOn=self.vpc,
86 )
87 self.rg1_vpc.add(self.out_vpc_cidr_block)
88
89 def mk_rg2_subnet(self):
90 self.rg2_subnet = cf.ResourceGroup("rg2_subnet")
91
92 self.public_subnet_list: List[ec2.Subnet] = list()
93 self.out_list_public_subnet_id: List[cf.Output] = list()
94 for az_ind in range(1, self.n_az_used + 1):
95 for subnet_ind in range(1, self.n_subnet_per_az_per_public_private + 1):
96 nth_pub_or_pri_subnet = (
97 az_ind - 1
98 ) * self.n_subnet_per_az_per_public_private + subnet_ind
99 logic_id = nth_pub_or_pri_subnet * 2 - 1
100 public_subnet = ec2.Subnet(
101 f"PublicSubnet{logic_id}",
102 p_CidrBlock="10.{}.{}.0/24".format(
103 self.vpc_cidr_seed,
104 logic_id,
105 ),
106 rp_VpcId=self.vpc.ref(),
107 p_AvailabilityZone=cf.GetAZs.n_th(az_ind),
108 p_MapPublicIpOnLaunch=True,
109 p_Tags=cf.Tag.make_many(
110 Name=f"{self.env_name}/public/{nth_pub_or_pri_subnet}",
111 ),
112 ra_DependsOn=self.vpc,
113 )
114 self.public_subnet_list.append(public_subnet)
115
116 out = cf.Output(
117 f"{public_subnet.id}Id",
118 Description=f"{public_subnet.id} Id",
119 Value=public_subnet.ref(),
120 Export=cf.Export(
121 "{}-{}-id".format(
122 self.env_name,
123 public_subnet.id.lower().replace("subnet", "-subnet-"),
124 ),
125 ),
126 DependsOn=public_subnet,
127 )
128 self.out_list_public_subnet_id.append(out)
129
130 self.rg2_subnet.add(public_subnet)
131 self.rg2_subnet.add(out)
132
133 self.private_subnet_list: List[ec2.Subnet] = list()
134 self.out_list_private_subnet_id: List[cf.Output] = list()
135 for az_ind in range(1, self.n_az_used + 1):
136 for subnet_ind in range(1, self.n_subnet_per_az_per_public_private + 1):
137 nth_pub_or_pri_subnet = (
138 az_ind - 1
139 ) * self.n_subnet_per_az_per_public_private + subnet_ind
140 logic_id = nth_pub_or_pri_subnet * 2
141 private_subnet = ec2.Subnet(
142 f"PrivateSubnet{logic_id}",
143 p_CidrBlock="10.{}.{}.0/24".format(
144 self.vpc_cidr_seed,
145 logic_id,
146 ),
147 rp_VpcId=self.vpc.ref(),
148 p_AvailabilityZone=cf.GetAZs.n_th(az_ind),
149 p_MapPublicIpOnLaunch=False,
150 p_Tags=cf.Tag.make_many(
151 Name=f"{self.env_name}/private/{nth_pub_or_pri_subnet}",
152 ),
153 ra_DependsOn=self.vpc,
154 )
155 self.private_subnet_list.append(private_subnet)
156
157 out = cf.Output(
158 f"{private_subnet.id}Id",
159 Description=f"{private_subnet.id} Id",
160 Value=private_subnet.ref(),
161 Export=cf.Export(
162 "{}-{}-id".format(
163 self.env_name,
164 private_subnet.id.lower().replace("subnet", "-subnet-"),
165 ),
166 ),
167 DependsOn=private_subnet,
168 )
169 self.out_list_private_subnet_id.append(out)
170
171 self.rg2_subnet.add(private_subnet)
172 self.rg2_subnet.add(out)
173
174 self.out_list_subnet_cidr_block: List[cf.Output] = list()
175 for subnet in self.public_subnet_list + self.private_subnet_list:
176 out = cf.Output(
177 f"{subnet.id}CidrBlock",
178 Description=f"{subnet.id} Cidr Block",
179 Value=subnet.p_CidrBlock,
180 Export=cf.Export(
181 "{}-{}-cidr-block".format(
182 self.env_name, subnet.id.lower().replace("subnet", "-subnet-")
183 ),
184 ),
185 DependsOn=subnet,
186 )
187 self.out_list_subnet_cidr_block.append(out)
188 self.rg2_subnet.add(out)
189
190 def mk_rg3_route(self):
191 """
192 For each VPC, we use ONE internet gateway and ONE nat gateway.
193
194 All public subnet use integer gateway.
195
196 All private subnet use nat gateway.
197 """
198 self.rg3_route = cf.ResourceGroup("rg3_route")
199
200 self.igw = ec2.InternetGateway(
201 "IGW",
202 p_Tags=cf.Tag.make_many(
203 Name=self.env_name,
204 ),
205 )
206 self.rg3_route.add(self.igw)
207
208 self.igw_attach_vpc = ec2.VPCGatewayAttachment(
209 "IGWAttachVpc",
210 rp_VpcId=self.vpc.ref(),
211 p_InternetGatewayId=self.igw.ref(),
212 ra_DependsOn=[self.vpc, self.igw],
213 )
214 self.rg3_route.add(self.igw_attach_vpc)
215
216 self.eip = ec2.EIP(
217 "EIP",
218 p_Domain="vpc",
219 p_Tags=cf.Tag.make_many(
220 Name=self.env_name,
221 ),
222 ra_DependsOn=self.vpc,
223 )
224 self.rg3_route.add(self.eip)
225
226 self.ngw = ec2.NatGateway(
227 "NGW",
228 rp_SubnetId=self.public_subnet_list[0].ref(),
229 p_AllocationId=self.eip.rv_AllocationId,
230 p_Tags=cf.Tag.make_many(
231 Name=self.env_name,
232 ),
233 ra_DependsOn=self.eip,
234 )
235 self.rg3_route.add(self.ngw)
236
237 # public / private route table
238 self.public_route_table = ec2.RouteTable(
239 "PublicRouteTable",
240 rp_VpcId=self.vpc.ref(),
241 p_Tags=cf.Tag.make_many(
242 Name=self.env_name,
243 ),
244 ra_DependsOn=self.vpc,
245 )
246 self.rg3_route.add(self.public_route_table)
247
248 self.public_route_default = ec2.Route(
249 "PublicRouteDefault",
250 rp_RouteTableId=self.public_route_table.ref(),
251 p_DestinationCidrBlock="0.0.0.0/0",
252 p_GatewayId=self.igw.ref(),
253 ra_DependsOn=[self.public_route_table, self.igw],
254 )
255 self.rg3_route.add(self.public_route_default)
256
257 for ind, subnet in enumerate(self.public_subnet_list):
258 route_table_association = ec2.SubnetRouteTableAssociation(
259 "PublicSubnet{}RouteTableAssociation".format(ind + 1),
260 rp_RouteTableId=self.public_route_table.ref(),
261 rp_SubnetId=subnet.ref(),
262 ra_DependsOn=[self.public_route_table, subnet],
263 )
264 self.rg3_route.add(route_table_association)
265
266 self.private_route_table = ec2.RouteTable(
267 "PrivateRouteTable",
268 rp_VpcId=self.vpc.ref(),
269 p_Tags=cf.Tag.make_many(
270 Name=self.env_name,
271 ),
272 ra_DependsOn=self.vpc,
273 )
274 self.rg3_route.add(self.private_route_table)
275
276 self.private_route_default = ec2.Route(
277 "PrivateRouteDefault",
278 rp_RouteTableId=self.private_route_table.ref(),
279 p_DestinationCidrBlock="0.0.0.0/0",
280 p_NatGatewayId=self.ngw.ref(),
281 ra_DependsOn=[self.private_route_table, self.ngw],
282 )
283 self.rg3_route.add(self.private_route_default)
284
285 for ind, subnet in enumerate(self.private_subnet_list):
286 route_table_association = ec2.SubnetRouteTableAssociation(
287 "PrivateSubnet{}RouteTableAssociation".format(ind + 1),
288 rp_RouteTableId=self.private_route_table.ref(),
289 rp_SubnetId=subnet.ref(),
290 ra_DependsOn=[self.private_route_table, subnet],
291 )
292 self.rg3_route.add(route_table_association)
293
294 def mk_pk4_security_group(self):
295 self.rg4_security_group = cf.ResourceGroup("rg4_security_group")
296
297 self.sg_of_allow_restricted_traffic_from_authorized_ip = ec2.SecurityGroup(
298 "SecurityGroupOfAllowRestrictedTrafficFromAuthorizedIp",
299 rp_GroupDescription="Allow restricted traffic from authorized ip usually workspace ip or developer home ip",
300 p_GroupName=f"{self.env_name}/sg/allow-restricted-traffic-from-authorized-ip",
301 p_VpcId=self.vpc.ref(),
302 p_SecurityGroupIngress=[
303 ec2.PropSecurityGroupIngress(
304 rp_IpProtocol="tcp",
305 p_FromPort=22,
306 p_ToPort=22,
307 p_CidrIp=f"{authorized_ip}/32",
308 )
309 for authorized_ip in self.sg_authorized_ips
310 ],
311 p_Tags=cf.Tag.make_many(
312 Name=f"{self.env_name}/sg/allow-restricted-traffic-from-authorized-ip"
313 ),
314 ra_DependsOn=self.vpc,
315 )
316 self.rg4_security_group.add(
317 self.sg_of_allow_restricted_traffic_from_authorized_ip
318 )
319
320 self.output_sg_id_of_allow_restricted_traffic_from_authorized_ip = cf.Output(
321 f"{self.sg_of_allow_restricted_traffic_from_authorized_ip.id}Id",
322 Description="Security Group ID",
323 Value=self.sg_of_allow_restricted_traffic_from_authorized_ip.rv_GroupId,
324 Export=cf.Export(
325 f"{self.env_name}-{self.sg_of_allow_restricted_traffic_from_authorized_ip.id}-id"
326 ),
327 DependsOn=self.sg_of_allow_restricted_traffic_from_authorized_ip,
328 )
329 self.rg4_security_group.add(
330 self.output_sg_id_of_allow_restricted_traffic_from_authorized_ip
331 )
332
333 self.sg_of_allow_all_traffic_from_authorized_ip = ec2.SecurityGroup(
334 "SecurityGroupOfAllowAllTrafficFromAuthorizedIp",
335 rp_GroupDescription="Allow All traffic from authorized ip usually workspace ip or developer home ip",
336 p_GroupName=f"{self.env_name}/sg/allow-all-traffic-from-authorized-ip",
337 p_VpcId=self.vpc.ref(),
338 p_SecurityGroupIngress=[
339 ec2.PropSecurityGroupIngress(
340 rp_IpProtocol="-1",
341 p_FromPort=-1,
342 p_ToPort=-1,
343 p_CidrIp=f"{authorized_ip}/32",
344 )
345 for authorized_ip in self.sg_authorized_ips
346 ],
347 p_Tags=cf.Tag.make_many(
348 Name=f"{self.env_name}/sg/allow-all-traffic-from-authorized-ip"
349 ),
350 ra_DependsOn=self.vpc,
351 )
352 self.rg4_security_group.add(self.sg_of_allow_all_traffic_from_authorized_ip)
353
354 self.output_sg_id_of_allow_all_traffic_from_authorized_ip = cf.Output(
355 f"{self.sg_of_allow_all_traffic_from_authorized_ip.id}Id",
356 Description="Security Group ID",
357 Value=self.sg_of_allow_all_traffic_from_authorized_ip.rv_GroupId,
358 Export=cf.Export(
359 f"{self.env_name}-{self.sg_of_allow_all_traffic_from_authorized_ip.id}-id"
360 ),
361 DependsOn=self.sg_of_allow_all_traffic_from_authorized_ip,
362 )
363 self.rg4_security_group.add(
364 self.output_sg_id_of_allow_all_traffic_from_authorized_ip
365 )
366
367 self.sg_of_allow_ssh_from_public_subnet = ec2.SecurityGroup(
368 "SecurityGroupOfAllowSSHFromPublicSubnet",
369 rp_GroupDescription="Allow ssh in from public subnet",
370 p_GroupName=f"{self.env_name}/sg/allow-ssh-from-public-subnet",
371 p_VpcId=self.vpc.ref(),
372 p_SecurityGroupIngress=[
373 ec2.PropSecurityGroupIngress(
374 rp_IpProtocol="tcp",
375 p_FromPort=22,
376 p_ToPort=22,
377 p_CidrIp=subnet.p_CidrBlock,
378 )
379 for subnet in self.public_subnet_list
380 ],
381 p_Tags=cf.Tag.make_many(
382 Name=f"{self.env_name}/sg/allow-ssh-from-public-subnet"
383 ),
384 ra_DependsOn=[
385 self.vpc,
386 ]
387 + self.public_subnet_list,
388 )
389 self.rg4_security_group.add(self.sg_of_allow_ssh_from_public_subnet)
390
391 self.output_sg_id_of_allow_ssh_from_public_subnet = cf.Output(
392 f"{self.sg_of_allow_ssh_from_public_subnet.id}Id",
393 Description="Security Group ID",
394 Value=self.sg_of_allow_ssh_from_public_subnet.rv_GroupId,
395 Export=cf.Export(
396 f"{self.env_name}-{self.sg_of_allow_ssh_from_public_subnet.id}-id"
397 ),
398 DependsOn=self.sg_of_allow_ssh_from_public_subnet,
399 )
400 self.rg4_security_group.add(self.output_sg_id_of_allow_ssh_from_public_subnet)
401
402 def mk_rg5_active_directory(self):
403 """
404 Active Directory is for client VPN endpoint authentication.
405 """
406 self.rg5_active_directory = cf.ResourceGroup("rg5_active_directory")
407
408 self.active_directory = directoryservice.MicrosoftAD(
409 "ActiveDirectory",
410 rp_Name="datalab-opensource.com",
411 rp_Password=self.active_directory_admin_password,
412 rp_VpcSettings=directoryservice.PropMicrosoftADVpcSettings(
413 rp_VpcId=self.vpc.ref(),
414 rp_SubnetIds=[
415 self.public_subnet_list[0].ref(),
416 self.public_subnet_list[
417 self.n_subnet_per_az_per_public_private
418 ].ref(),
419 ],
420 ),
421 p_Edition="Standard",
422 p_ShortName="DataLab",
423 ra_DependsOn=[
424 self.vpc,
425 self.public_subnet_list[0],
426 self.public_subnet_list[self.n_subnet_per_az_per_public_private],
427 ],
428 )
429 self.rg5_active_directory.add(self.active_directory)
430
431 self.out_active_directory_dns_1 = cf.Output(
432 "ActiveDirectoryDNSIpAddresses1",
433 Description=f"Active Directory DNS Ip Addresses 1",
434 Value=cf.Select(0, self.active_directory.rv_DnsIpAddresses),
435 Export=cf.Export(
436 "{}-active-directory-dns-1".format(
437 self.env_name,
438 ),
439 ),
440 DependsOn=self.active_directory,
441 )
442 self.rg5_active_directory.add(self.out_active_directory_dns_1)
443
444 self.out_active_directory_dns_2 = cf.Output(
445 "ActiveDirectoryDNSIpAddresses2",
446 Description=f"Active Directory DNS Ip Addresses 2",
447 Value=cf.Select(1, self.active_directory.rv_DnsIpAddresses),
448 Export=cf.Export(
449 "{}-active-directory-dns-2".format(
450 self.env_name,
451 ),
452 ),
453 DependsOn=self.active_directory,
454 )
455 self.rg5_active_directory.add(self.out_active_directory_dns_2)
456
457 def mk_rg6_vpn_endpoint(self):
458 """
459 Client VPN Endpoint, so user can use OpenVPN to connect to VPN and
460 hence have access to private subnet.
461 """
462 self.rg6_vpn_endpoint = cf.ResourceGroup("rg6_vpn_endpoint")
463
464 # Create client VPN endpoint
465 self.client_vpn_endpoint = ec2.ClientVpnEndpoint(
466 "ClientVpnEndpoint",
467 rp_ClientCidrBlock="10.254.0.0/16",
468 rp_AuthenticationOptions=[
469 ec2.PropClientVpnEndpointClientAuthenticationRequest(
470 rp_Type="directory-service-authentication",
471 p_ActiveDirectory=ec2.PropClientVpnEndpointDirectoryServiceAuthenticationRequest(
472 rp_DirectoryId=self.active_directory.ref(),
473 ),
474 ),
475 ],
476 rp_ServerCertificateArn=self.server_certificate_arn,
477 rp_ConnectionLogOptions=ec2.PropClientVpnEndpointConnectionLogOptions(
478 rp_Enabled=False,
479 ),
480 p_DnsServers=[
481 cf.Select(0, self.active_directory.rv_DnsIpAddresses),
482 cf.Select(1, self.active_directory.rv_DnsIpAddresses),
483 ],
484 p_SessionTimeoutHours=24,
485 p_SplitTunnel=True,
486 p_TagSpecifications=[
487 ec2.PropClientVpnEndpointTagSpecification(
488 rp_ResourceType="client-vpn-endpoint",
489 rp_Tags=cf.Tag.make_many(Name=self.env_name),
490 )
491 ],
492 p_VpcId=self.vpc.ref(),
493 p_SecurityGroupIds=[
494 self.vpc.rv_DefaultSecurityGroup,
495 self.sg_of_allow_all_traffic_from_authorized_ip.ref(),
496 ],
497 ra_DependsOn=[
498 self.vpc,
499 self.sg_of_allow_all_traffic_from_authorized_ip,
500 self.active_directory,
501 ],
502 )
503 self.rg6_vpn_endpoint.add(self.client_vpn_endpoint)
504
505 # associate client vpn to all public subnets
506 self.client_vpn_target_network_association_list: List[
507 ec2.ClientVpnTargetNetworkAssociation
508 ] = list()
509 indices = [
510 i * self.n_subnet_per_az_per_public_private for i in range(self.n_az_used)
511 ]
512 for ind in indices:
513 public_subnet = self.public_subnet_list[ind]
514 association = ec2.ClientVpnTargetNetworkAssociation(
515 f"ClientVpnTargetNetworkAssociation{ind}",
516 rp_ClientVpnEndpointId=self.client_vpn_endpoint.ref(),
517 rp_SubnetId=public_subnet.ref(),
518 ra_DependsOn=[
519 self.client_vpn_endpoint,
520 public_subnet,
521 ],
522 )
523 self.client_vpn_target_network_association_list.append(association)
524 self.rg6_vpn_endpoint.add(association)
525
526 # set client VPN endpoint to use active directory for authentication
527 self.client_vpn_auth_rule = ec2.ClientVpnAuthorizationRule(
528 "ClientVpnAuthRule",
529 rp_ClientVpnEndpointId=self.client_vpn_endpoint.ref(),
530 rp_TargetNetworkCidr=self.vpc.rv_CidrBlock,
531 p_AuthorizeAllGroups=True,
532 )
533 self.rg6_vpn_endpoint.add(self.client_vpn_auth_rule)
534
535 # configure DHCP for VPC
536 self.dhcp_options = ec2.DHCPOptions(
537 "DHCPOption",
538 p_DomainName=self.active_directory.rp_Name,
539 p_DomainNameServers=[
540 cf.Select(0, self.active_directory.rv_DnsIpAddresses),
541 cf.Select(1, self.active_directory.rv_DnsIpAddresses),
542 ],
543 p_Tags=cf.Tag.make_many(Name=f"{self.env_name}-vpc"),
544 ra_DependsOn=[
545 self.active_directory,
546 ],
547 )
548 self.rg6_vpn_endpoint.add(self.dhcp_options)
549
550 self.vpc_dhcp_options_association = ec2.VPCDHCPOptionsAssociation(
551 "VPCDHCPOptionAssociation",
552 rp_VpcId=self.vpc.ref(),
553 rp_DhcpOptionsId=self.dhcp_options.ref(),
554 ra_DependsOn=[
555 self.vpc,
556 self.dhcp_options,
557 ],
558 )
559 self.rg6_vpn_endpoint.add(self.vpc_dhcp_options_association)
Reference#
Microsoft Training - Manage Windows Server file servers: 微软介绍 WFS 的官方文档.
Accessing SMB file shares remotely with Amazon FSx for Windows File Server: 一篇叫你怎么使用 FSx for WFS 的官方博客, 建议精读.
fsxpathlib: Python 面向对象的客户端, 是对 SMB 协议的封装.