AWS CDK を使って独自ドメインの静的サイトを S3 でホスティングする環境を構築してみた
2019-12-18AWS CDK を使って独自ドメインを使用した静的サイトを S3 でホスティングするための環境を構築してみたので、今回はその話です。
前置き
これは
AWS初心者 Advent Calendar 2019
18 日目の記事です。
4 日目に引き続き 2 枠目になりますが、空いていたので滑り込みで入れさせてもらいました。
4 日目に書いた記事はこちら。
概要
タイトルの通り、 AWS CDK を使って独自ドメインを使用した静的サイトを S3 でホスティングするための環境を構築します。
本当は Hugo という静的サイトジェネレータの CI/CD 環境を CDK を使って整えようとしていたのですが、一旦その前段階として、とりあえず S3 で独自ドメインの静的サイトをホスティングする環境を構築してみます。
最終的にできあがる環境は、下図のようになります。
めちゃくちゃシンプルです。
前提
- AWS CDK がインストールされている (執筆時点で最新の
1.18.0
1.19.0
を使用します)
※この記事公開当日に1.19.0
がリリースされました。 - Route 53 で独自ドメインのホストゾーンが作成されている
今回は既存のホストゾーンに対してレコードを追加するような構成を考えます。
やること
流れとしては下記の通りです。
- CDK プロジェクトを作成
- 実装
- デプロイ
とりあえず分けましたが、一番ボリュームが大きいのが 2. 実装 になります。 (それはそう)
では、一つずつ順番にやっていきます。
なお、ここから紹介するコードについては GitHub に置いていますので、適当にいじってみてください。 (アドバイスもいただけると幸いです)
1. CDK プロジェクトを作成
まずは CDK プロジェクトを作成します。
$ mkdir static-website-sample
$ cd static-website-sample
$ cdk init app --language=typescript
$ tree . -L 1
.
├── README.md
├── bin
│ └── static-website-sample.ts
├── cdk.context.json
├── cdk.json
├── cdk.out
├── jest.config.js
├── lib
│ └── static-website-sample-stack.ts
├── node_modules
├── package-lock.json
├── package.json
├── test
└── tsconfig.json
こんな感じのディレクトリ構成が作られます。
CDK のロジックは lib/static-website-sample-stack.ts
に記述していきます。
2. 実装
では、メインの実装です。
必要なモジュールをインストール
実装を始める前に、必要なモジュールをインストールします。今回使用するモジュールは、 @aws-cdk/aws-s3
, @aws-cdk/aws-s3-deployment
, @aws-cdk/aws-route53
, @aws-cdk/aws-route53-targets
, js-yaml
, @types/js-yaml
です。
$ npm install @aws-cdk/aws-s3 @aws-cdk/aws-s3-deployment @aws-cdk/aws-route53 @aws-cdk/aws-route53-targets js-yaml @types/js-yaml
独自の設定ファイルの読み込み
今回のポイントとして、独自ドメインは動的に変更できるように独自の設定ファイルに分離させます。
ということで、まずはその独自の設定ファイルから実装していきます。
設定ファイル自体は YAML 形式で stack-config.yml
として作成します。
common:
region: ap-northeast-1
route53:
zone: michimani.net
zone_id: ABCD1234567890
sub_domain: static-website-sample
これらの値の定義を lib/Config.ts
というファイルで作成します。内容は下記のとおりです。
import yaml = require('js-yaml');
import fs = require('fs');
export interface commonConfig {
region: string;
}
export interface Route53Config {
zone: string;
zone_id: string;
sub_domain?: string;
}
export interface StackConfig {
common: commonConfig,
route53: Route53Config
}
const stackConfig: StackConfig = yaml.safeLoad(fs.readFileSync('stack-config.yml', {encoding: 'utf-8'}));
export class StackConfig {
constructor() {
const stackConfig = yaml.safeLoad(fs.readFileSync('stack-config.yml', {encoding: 'utf-8'}));
return stackConfig;
}
}
この設定を読み込ませるために、 bin/static-website-sample.ts
に下記のように実装します。
#!/usr/bin/env node
import 'source-map-support/register';
import cdk = require('@aws-cdk/core');
import { StaticWebsiteSampleStack } from '../lib/static-website-sample-stack';
import * as Config from '../lib/Config';
const app = new cdk.App();
const stackConfig = new Config.StackConfig();
new StaticWebsiteSampleStack(app, 'StaticWebsiteSampleStack', stackConfig, {
env: {
region: stackConfig.common.region
},
});
また、 StaticWebsiteSampleStack
で stackConfig
を受け取れるように、 lib/static-website-sample-stack.ts
のコンストラクタに変更を加えます。
+ import * as config from './Config';
export class StaticWebsiteSampleStack extends cdk.Stack {
- constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
+ constructor(scope: cdk.Construct, id: string, stackConfig: config.StackConfig, props?: cdk.StackProps) {
super(scope, id, props);
これで、 stack-config.yml
に記述した値を使用できるようになりました。
可変の値の設定方法については色々なやり方があると思うので、もっと良い方法があればぜひ教えていただきたいです。
各リソースを生成
諸々の準備が整ったので、各リソースを生成するためのコードを lib/static-website-sample-stack.ts
に書いていきます。
まず、最初にインストールしたモジュールを読み込みます。
import s3 = require('@aws-cdk/aws-s3');
import s3deploy = require('@aws-cdk/aws-s3-deployment');
import route53 = require('@aws-cdk/aws-route53');
import route53targets = require('@aws-cdk/aws-route53-targets');
また、今回は独自ドメインを使用する際に、サブドメインを使用する場合と使わない場合があります。それによって作成するリソースに設定する値も変わってくるので、サブドメインを使用するかどうかをフラグで持っておきます。
const useSubDomain: boolean = (typeof stackConfig.route53.sub_domain !== 'undefined' && stackConfig.route53.sub_domain !== '' && stackConfig.route53.sub_domain !== null);
リソース作成の順番としては、次のようになります。
- S3 バケットの作成
- バケット内に静的ファイルを設置
- Route 53 で S3 バケットに対するレコードを追加
- 確認
では、順番に実装部分を見ていきます。
まずは S3 バケットの作成です。
const bucketName: string = (useSubDomain === true) ? `${stackConfig.route53.sub_domain}.${stackConfig.route53.zone}` : stackConfig.route53.zone;
const bucket: s3.Bucket = new s3.Bucket(this, 'StaticWebsiteSampleBucket', {
bucketName,
publicReadAccess: true,
websiteIndexDocument: 'index.html',
websiteErrorDocument: 'error.html',
removalPolicy: cdk.RemovalPolicy.DESTROY
});
ポイントとしては、 バケット名を独自ドメインと同じ形式にする というところです。これは独自ドメインを使用して S3 で静的サイトをホスティングする際の条件になっています。
続いて、このバケットに HTML などの静的ファイルを設置します。
今回は ./public
というディレクトリを作り、その中に index.html
と error.html
を作成しておきます。
$ tree ./public
./public
├── error.html
└── index.html
実際にオブジェクトを設置するコードは下記のようになります。
const sampleHtmlPut = new s3deploy.BucketDeployment(this, 'SampleHtmlDeploy', {
destinationBucket: bucket,
sources: [s3deploy.Source.asset('./public')]
});
ここで destinationBucket
には先程作成した bucket
を指定しています。 CDK ではこのような形で各リソースの依存関係を簡単に構築することができます。
最後に、 Route 53 で独自ドメインのレコード設定を実装します。
const myZone: route53.IHostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'MyZone', {
zoneName: stackConfig.route53.zone,
hostedZoneId: stackConfig.route53.zone_id
});
const record: route53.ARecordProps = {
zone: myZone,
target: route53.AddressRecordTarget.fromAlias(new route53targets.BucketWebsiteTarget(bucket)),
recordName: (useSubDomain === true) ? stackConfig.route53.sub_domain : stackConfig.route53.zone
}
const hostRecord = new route53.ARecord(this, 'StaticWebsiteSampleRecord', record);
前半部分では、ゾーン名とゾーン ID から既存のホストゾーンをインポートしています。
そして、そのゾーンに対してレコードを追加しています。
先ほどと同じように、レコードのエイリアスとなる S3 バケットの情報は、最初に作成した bucket
を渡すことで成立しています。
recordName
の値は、サブドメインを使用する場合はその ホスト部分のみ を設定します。サブドメインを使用しない場合は、 recordName
を省略するか、ゾーン名を設定します。
実装は以上となります。
ちなみに、 bin/static-website-sample.ts
内で明示的にリージョンを指定してるのは、 Route 53 のレコードのエイリアスとして S3 バケットを指定する際の制限となります。
指定せずにビルドしようとすると、下記のようなエラーが出ます。
Cannot use an S3 record alias in region-agnostic stacks. You must specify a specific region when you define the stack (see https://docs.aws.amazon.com/cdk/latest/guide/environments.html)
Subprocess exited with error 1
3. デプロイ
実装が完了したので、デプロイします。
デプロイコマンドは cdk deploy
ですが、その前に、実際に出力される CloudFormation テンプレートを cdk synth
コマンドで出力してみます。
$ cdk synth
{ sourceHash:
'6416c21be320b522db64c705872c0a54d788e3df57b34a5f0d1e8602d7521430' }
Resources:
StaticWebsiteSampleBucketABCDEFGH:
Type: AWS::S3::Bucket
Properties:
BucketName: static-site-sample.michimani.net
WebsiteConfiguration:
ErrorDocument: error.html
IndexDocument: index.html
UpdateReplacePolicy: Delete
DeletionPolicy: Delete
Metadata:
aws:cdk:path: StaticWebsiteSampleStack/StaticWebsiteSampleBucket/Resource
StaticWebsiteSampleBucketPolicyF002703A:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: StaticWebsiteSampleBucketABCDEFGH
PolicyDocument:
Statement:
- Action: s3:GetObject
Effect: Allow
Principal: "*"
Resource:
Fn::Join:
- ""
- - Fn::GetAtt:
- StaticWebsiteSampleBucketABCDEFGH
- Arn
- /*
Version: "2012-10-17"
Metadata:
aws:cdk:path: StaticWebsiteSampleStack/StaticWebsiteSampleBucket/Policy/Resource
...
という感じで出力されます。
CDK のコードとしては 40 行程度ですが、 CloudFormation テンプレートにすると 200 行近くになります。
テンプレートを出力してみると、あらためて CDK の恩恵を感じられます。
では、 cdk deploy
コマンドでデプロイしてみます。
$ cdk deploy
{ sourceHash:
'6416c21be320b522db64c705872c0a54d788e3df57b34a5f0d1e8602d7521430' }
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
IAM Statement Changes
┌───┬─────────────────────────────────────────────────────────────────────────────┬────────┬─────────────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────┬───────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼─────────────────────────────────────────────────────────────────────────────┼────────┼─────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────┼───────────┤
│ + │ ${Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999/ServiceRole.A │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │
│ │ rn} │ │ │ │ │
├───┼─────────────────────────────────────────────────────────────────────────────┼────────┼─────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────┼───────────┤
│ + │ ${StaticWebsiteSampleBucket.Arn} │ Allow │ s3:Abort* │ AWS:${Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999/ServiceRol │ │
│ │ ${StaticWebsiteSampleBucket.Arn}/* │ │ s3:DeleteObject* │ e} │ │
│ │ │ │ s3:GetBucket* │ │ │
│ │ │ │ s3:GetObject* │ │ │
│ │ │ │ s3:List* │ │ │
│ │ │ │ s3:PutObject* │ │ │
├───┼─────────────────────────────────────────────────────────────────────────────┼────────┼─────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────┼───────────┤
│ + │ ${StaticWebsiteSampleBucket.Arn}/* │ Allow │ s3:GetObject │ * │ │
├───┼─────────────────────────────────────────────────────────────────────────────┼────────┼─────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────┼───────────┤
│ + │ arn:${AWS::Partition}:s3:::${AssetParameterscb52f748a3861e8690fd5f1fc2d76ed │ Allow │ s3:GetBucket* │ AWS:${Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999/ServiceRol │ │
│ │ 9a8bfc149334ddca4e31d2ba441ff5634S3Bucket5A5BA922} │ │ s3:GetObject* │ e} │ │
│ │ arn:${AWS::Partition}:s3:::${AssetParameterscb52f748a3861e8690fd5f1fc2d76ed │ │ s3:List* │ │ │
│ │ 9a8bfc149334ddca4e31d2ba441ff5634S3Bucket5A5BA922}/* │ │ │ │ │
└───┴─────────────────────────────────────────────────────────────────────────────┴────────┴─────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬────────────────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│ │ Resource │ Managed Policy ARN │
├───┼────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴────────────────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Do you wish to deploy these changes (y/n)? y
StaticWebsiteSampleStack: deploying...
StaticWebsiteSampleStack: creating CloudFormation changeset...
0/9 | 9:22:15 PM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata
0/9 | 9:22:15 PM | CREATE_IN_PROGRESS | AWS::IAM::Role | Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999/ServiceRole (CustomCDKBucketDeploymentRANDOMSTRING99999999999999999999ServiceRole89A01265)
0/9 | 9:22:15 PM | CREATE_IN_PROGRESS | AWS::Route53::RecordSet | StaticWebsiteSampleRecord (StaticWebsiteSampleRecord0C67AEAA)
0/9 | 9:22:15 PM | CREATE_IN_PROGRESS | AWS::S3::Bucket | StaticWebsiteSampleBucket (StaticWebsiteSampleBucketED610F77)
0/9 | 9:22:16 PM | CREATE_IN_PROGRESS | AWS::IAM::Role | Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999/ServiceRole (CustomCDKBucketDeploymentRANDOMSTRING99999999999999999999ServiceRole89A01265) Resource creation Initiated
0/9 | 9:22:17 PM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated
0/9 | 9:22:17 PM | CREATE_IN_PROGRESS | AWS::Route53::RecordSet | StaticWebsiteSampleRecord (StaticWebsiteSampleRecord0C67AEAA) Resource creation Initiated
1/9 | 9:22:17 PM | CREATE_COMPLETE | AWS::CDK::Metadata | CDKMetadata
1/9 | 9:22:17 PM | CREATE_IN_PROGRESS | AWS::S3::Bucket | StaticWebsiteSampleBucket (StaticWebsiteSampleBucketED610F77) Resource creation Initiated
2/9 | 9:22:32 PM | CREATE_COMPLETE | AWS::IAM::Role | Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999/ServiceRole (CustomCDKBucketDeploymentRANDOMSTRING99999999999999999999ServiceRole89A01265)
3/9 | 9:22:38 PM | CREATE_COMPLETE | AWS::S3::Bucket | StaticWebsiteSampleBucket (StaticWebsiteSampleBucketED610F77)
3/9 | 9:22:41 PM | CREATE_IN_PROGRESS | AWS::IAM::Policy | Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999/ServiceRole/DefaultPolicy (CustomCDKBucketDeploymentRANDOMSTRING99999999999999999999ServiceRoleDefaultPolicy88902FDF)
3/9 | 9:22:41 PM | CREATE_IN_PROGRESS | AWS::S3::BucketPolicy | StaticWebsiteSampleBucket/Policy (StaticWebsiteSampleBucketPolicyF002703A)
3/9 | 9:22:42 PM | CREATE_IN_PROGRESS | AWS::IAM::Policy | Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999/ServiceRole/DefaultPolicy (CustomCDKBucketDeploymentRANDOMSTRING99999999999999999999ServiceRoleDefaultPolicy88902FDF) Resource creation Initiated
3/9 | 9:22:43 PM | CREATE_IN_PROGRESS | AWS::S3::BucketPolicy | StaticWebsiteSampleBucket/Policy (StaticWebsiteSampleBucketPolicyF002703A) Resource creation Initiated
4/9 | 9:22:43 PM | CREATE_COMPLETE | AWS::S3::BucketPolicy | StaticWebsiteSampleBucket/Policy (StaticWebsiteSampleBucketPolicyF002703A)
5/9 | 9:22:58 PM | CREATE_COMPLETE | AWS::IAM::Policy | Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999/ServiceRole/DefaultPolicy (CustomCDKBucketDeploymentRANDOMSTRING99999999999999999999ServiceRoleDefaultPolicy88902FDF)
5/9 | 9:23:01 PM | CREATE_IN_PROGRESS | AWS::Lambda::Function | Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999 (CustomCDKBucketDeploymentRANDOMSTRING9999999999999999999981C01536)
5/9 | 9:23:03 PM | CREATE_IN_PROGRESS | AWS::Lambda::Function | Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999 (CustomCDKBucketDeploymentRANDOMSTRING9999999999999999999981C01536) Resource creation Initiated
6/9 | 9:23:04 PM | CREATE_COMPLETE | AWS::Lambda::Function | Custom::CDKBucketDeploymentRANDOMSTRING99999999999999999999 (CustomCDKBucketDeploymentRANDOMSTRING9999999999999999999981C01536)
6/9 | 9:23:06 PM | CREATE_IN_PROGRESS | Custom::CDKBucketDeployment | SampleHtmlDeploy/CustomResource/Default (SampleHtmlDeployCustomResource33CAFD7F)
7/9 | 9:23:21 PM | CREATE_COMPLETE | AWS::Route53::RecordSet | StaticWebsiteSampleRecord (StaticWebsiteSampleRecord0C67AEAA)
7/9 | 9:23:45 PM | CREATE_IN_PROGRESS | Custom::CDKBucketDeployment | SampleHtmlDeploy/CustomResource/Default (SampleHtmlDeployCustomResource33CAFD7F) Resource creation Initiated
8/9 | 9:23:46 PM | CREATE_COMPLETE | Custom::CDKBucketDeployment | SampleHtmlDeploy/CustomResource/Default (SampleHtmlDeployCustomResource33CAFD7F)
9/9 | 9:23:47 PM | CREATE_COMPLETE | AWS::CloudFormation::Stack | StaticWebsiteSampleStack
✅ StaticWebsiteSampleStack
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:123412341234:stack/StaticWebsiteSampleStack/9857da30-1ffe-11ea-a5a2-000000000000
こんな感じで、生成されるリソースが表で出力され、 Do you wish to deploy these changes (y/n)?
と聞かれます。そこで y
をタイプすると、デプロイが開始されます。
このタイミングで、マネジメントコンソール上でもスタックのイベントが進行していることを確認することができます。
4. 確認
では、ちゃんとデプロイできているのか確認してみます。
今回は static-site-sample.michimani.net
のドメインで静的サイトをホスティングしてみたので、そのドメインで確認してみます。
通常ページ
$ http http://static-site-sample.michimani.net
HTTP/1.1 200 OK
Content-Length: 392
Content-Type: text/html
Date: Mon, 16 Dec 2019 12:59:06 GMT
ETag: "770c6c57182c470d8748793c36c8d070"
Last-Modified: Mon, 16 Dec 2019 12:23:42 GMT
Server: AmazonS3
x-amz-id-2: ZuSWrclJxk4uQ9XCaNurCnUZqfV/O48OrHUOHpkXdrfMsRYpJ/gCWF6MpMfdRynasbQHpKXUO/0=
x-amz-request-id: 614F5DF6A50B13A8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Static Web Site Sample</title>
</head>
<body>
<h1>Static Web Site Sample</h1>
<small>
This is a sample of static web page that crated by AWS CDK project.
</small>
</body>
</html>
エラーページ
$ http http://static-site-sample.michimani.net/not-exists-path/
HTTP/1.1 404 Not Found
Content-Length: 405
Content-Type: text/html
Date: Mon, 16 Dec 2019 13:00:00 GMT
ETag: "88431d0e820cb2e640475abbae30e298"
Last-Modified: Mon, 16 Dec 2019 12:23:42 GMT
Server: AmazonS3
x-amz-error-code: NoSuchKey
x-amz-error-detail-Key: not-exists-path/index.html
x-amz-error-message: The specified key does not exist.
x-amz-id-2: tysmHKDiqPdDuPRmQ2aKSjm1H5TfrB8GBJYKhZ9VZBVbUhQBlGKeXFPIvEWLi98rbwaZwA98YLM=
x-amz-request-id: CB5A4EA6A195621A
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Static Web Site Sample</title>
</head>
<body>
<h1>Static Web Site Sample - Error Page</h1>
<small>
This is a sample of static web page that crated by AWS CDK project.
</small>
</body>
</html>
ちゃんとデプロイできてますね。
まとめ
AWS CDK を使って独自ドメインを使用した静的サイトを S3 でホスティングするための環境を構築してみた話でした。
シンプルな構成ではありますが、コマンドひとつでリソースが立ち上がり、各種連携がされるのは、やはり IaaS の良いところですね。
今回は CloudFormation テンプレートを直接書くのではなく、 AWS CDK を利用しました。
CDK を利用することによって、 TypeScript や Python, Java といった言語で CloudFormation テンプレートを作成することができます。テンプレート用の YAML を書くのはちょっと難しい… と感じている人 (僕ですが) にも、 CDK であれば IaaS デビューしやすいかなと思います。
ただ、 CDK はあくまでも CloudFormation テンプレートを生成するために後から出てきたツールのため、生の CloudFormation テンプレートでは実現できても CDK では実現できない機能・リソースもあります。
例えば、 CDK の CloudFront モジュールは現在 (1.18.0
1.19.0
) は developer preview (public beta) となっています。そのため、 CloudFront を S3 の前段において SSL 化された静的サイトを構築しようとすると、 CDK では (production では) 実現できません。(Public Preview のため)
そのほか、より複雑な構成を実現しようとすると、 CDK では難しいという場合もあるようです。
これに関しては CDK の GA 記念ミートアップの際にも話が出ていました。
とは行っても CDK のアップデートスピードはかなり早いので、今後のアップデートには期待したいです。
少し脱線しましたが、 CDK は IaaS デビューには凄くいいツールだと思うので、ぜひ簡単なリソース作成から試してみてはいかがでしょうか。
comments powered by Disqus