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#