Angular×AWSでWorldにHello!したい

# はじめに
言語やフレームワークの勉強をしていてはじめに出力する内容は決まって「Hello World!」だと思います。ただ、私は実行してみていつも思います。

**いや、localにはHello!出来てるけどWorldにはHello!出来てなくない？**

ということで今回はAngular×AWSでWorldにHello!するまでの流れを記載しました。

# 目指すアーキテクチャ
![spa_architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/937419/15cbc70e-610b-e4e1-d7c5-5e9ce84a9c74.png)
アーキテクチャとしてはS3に入れたAngularのアプリをCloudFrontでホスティングする仕組みをゴールとします。
# まずはlocalにHello!
WolrdにHello!する前にまずはlocalにHello!出来てないと始まらないので、まずはlocalにHello!します。
これに関しては、Angularの公式のセットアップを見るのをおすすめしますが、ざっくりと手順をまとめました。
[Angular公式](https://angular.jp/guide/setup-local)

## 環境構築
まずは[Node.js](https://nodejs.org/en/)をインストールします。
※Node.jsのバージョンを管理されたい方はnvm経由でインストールするようにしましょう。
インストールするとNode.jsと共にnpmパッケージマネージャーがインストールされるのでnpmを使ってAngularをインストールしていきます。

```
npm install -g @angular/cli
```
これでAngularがインストール出来ました。
次にAngularのアプリを作成していきます。

```
ng new my-app
```
これで新しいAngularアプリケーションが作成されました。簡単ですね。
それでは、作成されたアプリケーションを実行してみましょう。

```
cd my-app
ng serve --open
```
これでlocalにHello!は完了です。
# ビルドしてみよう
これまでの処理でアプリケーションの実行までは出来ました。しかし、これをホスティングするためにはビルドする必要があります。
ビルドは下記のコマンドで出来ます。

```
ng build --aot
```
このコマンドを実行するとアプリケーションのルートディレクトリにdistというディレクトリが出来ていると思います。
dist/my-app配下のファイルがホスティングするべきファイル群になります。
これをS3に配置することでどこからでもアプリケーションを見ることが出来るようになります。
# AWS周りの設定をしよう
上までの手順でアプリケーション本体のファイルは作成することが出来ました。
それでは次に基盤周りの設定をしていきましょう。
今回は作成をより簡略化するためにCloudFormationのテンプレートでリソースを作っていきます。
テンプレートはyaml形式で記載しています。
## パラメータ設定
まずは、パラメータを設定します。
S3のバケット名やアプリを入れるディレクトリ名は自由に変えたいケースが多いのでパラメータ化しておきます。

```yaml:template.yml
Parameters:
  AngularBucketName:
    Type: String
    Default: "angular-app-bucket"
    Description: "S3 bucket to Create"
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9_-]*"
  DirName:
    Type: String
    Default: "app"
    Description: "S3 bucket to Create"
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9_-]*"
```
## S3の作成
次にS3のバケットを作成します。
今回アプリはバケット内にディレクトリを切ってその中に入れたいと思っているので、ディレクトリ作成までテンプレートで行います。

まずはバケット作成です。

```yaml:template.yml
...
省略
...
Resources:
  AngularBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref AngularBucketName
```
これでS3にバケットが作成できます。

それでは次にバケット内にディレクトリを作成します。
ディレクトリはテンプレートで直接作ることが出来ないのでカスタムリソースでLambdaを作成してそのLambdaでディレクトリを作成していきます。

```yaml:template.yml
...
省略
...
  AngularDir:
    Type: Custom::S3CustomResource
    Properties:
      ServiceToken: !GetAtt AWSLambdaFunction.Arn
      the_bucket: !Ref AngularBucket
      dir_to_create: !Ref DirName
  AWSLambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      Description: "Work with S3 bucket"
      FunctionName: !Sub '${AWS::StackName}-${AWS::Region}-lambda'
      Handler: index.handler
      Role: !GetAtt AWSLambdaExecutionRole.Arn
      Timeout: 360
      Runtime: python3.6
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          def handler(event, context):
            # Init ...
            the_event = event['RequestType']
            print("The event is: ", str(the_event))
            response_data = {}
            s_3 = boto3.client('s3')
            # Retrieve parameters
            the_bucket = event['ResourceProperties']['the_bucket']
            dir_to_create = event['ResourceProperties']['dir_to_create']
            try:
              if the_event in ('Create', 'Update'):
                print("Requested folders: ", str(dir_to_create))
                print("Creating: ", str(dir_to_create))
                s_3.put_object(Bucket=the_bucket, Key=(dir_to_create + '/'))
              elif the_event == 'Delete':
                print("Deleting S3 content")
                b_operator = boto3.resource('s3')
                b_operator.Bucket(str(the_bucket)).objects.all().delete()
              print("Operation successfull")
              cfnresponse.send(event,context,cfnresponse.SUCCESS, response_data)
            except Exception as e:
              print("Operation failed...")
              print(str(e))
              response_data['Data'] = str(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
  AWSLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action:
          - sts:AssumeRole
          Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
        Version: '2012-10-17'
      Path: "/"
      Policies:
      - PolicyDocument:
          Statement:
          - Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Effect: Allow
            Resource: arn:aws:logs:*:*:*
          Version: '2012-10-17'
        PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-CW
      - PolicyDocument:
          Statement:
          - Action:
            - s3:PutObject
            - s3:DeleteObject
            - s3:List*
            Effect: Allow
            Resource:
            - !Sub arn:aws:s3:::${AngularBucket}/*
            - !Sub arn:aws:s3:::${AngularBucket}
          Version: '2012-10-17'
        PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-S3
      RoleName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambdaExecutionRole
```
非常に長いコードになっていますが、やっていることとしては三つです。

+ CloudFormationが実行されるたびに実行されるカスタムリソースを定義
+ 実行されるラムダの定義
+ 実行されるラムダのRoleの定義

## CloudFrontの作成
これまでの処理でS3バケットとディレクトリまで作成できました。
それでは次にディレクトリに入っているデータをホスティングするCloudFrontを作っていきましょう。

```yaml:template.yml
...
省略
...
  AngularBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AngularBucket
      PolicyDocument:
        Statement:
          - Action: s3:GetObject
            Effect: Allow
            Resource: !Sub arn:aws:s3:::${AngularBucket}/${DirName}/*
            Principal:
              AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}
  CloudForntDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
        - DomainName: !GetAtt AngularBucket.DomainName
          Id: AngularOrigin
          OriginPath: !Sub /${DirName}
          S3OriginConfig:
            OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
        Enabled: 'true'
        DefaultRootObject: index.html
        DefaultCacheBehavior:
          AllowedMethods:
          - GET
          - HEAD
          TargetOriginId: AngularOrigin
          ForwardedValues:
            QueryString: 'false'
          ViewerProtocolPolicy: allow-all
        PriceClass: PriceClass_200
  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Ref AWS::StackName
```
最初にAngularBucketPolicyがあり、「おや？」と思った方もいらっしゃるかもしれません。
こちらは、CloudFrontからS3にアクセスするための設定です。
S3のデータは公開設定にしていない限り外部から見ることが出来ないのでOriginAccessIdentityを設定する必要があります。

これでテンプレートの準備は出来ました。
<details><summary>template.yamlの全体</summary><div>

```yaml:template.yml
Parameters:
  AngularBucketName:
    Type: String
    Default: "angular-app-bucket"
    Description: "S3 bucket to Create"
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9_-]*"
  DirName:
    Type: String
    Default: "app"
    Description: "S3 bucket to Create"
    AllowedPattern: "[a-zA-Z][a-zA-Z0-9_-]*"
Resources:
  AngularBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref AngularBucketName
  AngularBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AngularBucket
      PolicyDocument:
        Statement:
          - Action: s3:GetObject
            Effect: Allow
            Resource: !Sub arn:aws:s3:::${AngularBucket}/${DirName}/*
            Principal:
              AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}
  AngularDir:
    Type: Custom::S3CustomResource
    Properties:
      ServiceToken: !GetAtt AWSLambdaFunction.Arn
      the_bucket: !Ref AngularBucket
      dir_to_create: !Ref DirName
  AWSLambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      Description: "Work with S3 bucket"
      FunctionName: !Sub '${AWS::StackName}-${AWS::Region}-lambda'
      Handler: index.handler
      Role: !GetAtt AWSLambdaExecutionRole.Arn
      Timeout: 360
      Runtime: python3.6
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          def handler(event, context):
            # Init ...
            the_event = event['RequestType']
            print("The event is: ", str(the_event))
            response_data = {}
            s_3 = boto3.client('s3')
            # Retrieve parameters
            the_bucket = event['ResourceProperties']['the_bucket']
            dir_to_create = event['ResourceProperties']['dir_to_create']
            try:
              if the_event in ('Create', 'Update'):
                print("Requested folders: ", str(dir_to_create))
                print("Creating: ", str(dir_to_create))
                s_3.put_object(Bucket=the_bucket, Key=(dir_to_create + '/'))
              elif the_event == 'Delete':
                print("Deleting S3 content")
                b_operator = boto3.resource('s3')
                b_operator.Bucket(str(the_bucket)).objects.all().delete()
              print("Operation successfull")
              cfnresponse.send(event,context,cfnresponse.SUCCESS, response_data)
            except Exception as e:
              print("Operation failed...")
              print(str(e))
              response_data['Data'] = str(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
  AWSLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action:
          - sts:AssumeRole
          Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
        Version: '2012-10-17'
      Path: "/"
      Policies:
      - PolicyDocument:
          Statement:
          - Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Effect: Allow
            Resource: arn:aws:logs:*:*:*
          Version: '2012-10-17'
        PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-CW
      - PolicyDocument:
          Statement:
          - Action:
            - s3:PutObject
            - s3:DeleteObject
            - s3:List*
            Effect: Allow
            Resource:
            - !Sub arn:aws:s3:::${AngularBucket}/*
            - !Sub arn:aws:s3:::${AngularBucket}
          Version: '2012-10-17'
        PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-S3
      RoleName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambdaExecutionRole
  CloudForntDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
        - DomainName: !GetAtt AngularBucket.DomainName
          Id: AngularOrigin
          OriginPath: !Sub /${DirName}
          S3OriginConfig:
            OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
        Enabled: 'true'
        DefaultRootObject: index.html
        DefaultCacheBehavior:
          AllowedMethods:
          - GET
          - HEAD
          TargetOriginId: AngularOrigin
          ForwardedValues:
            QueryString: 'false'
          ViewerProtocolPolicy: allow-all
        PriceClass: PriceClass_200
  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Ref AWS::StackName
```
</div></details>
このテンプレートでCloudFormationを使ってリソースを作ると基盤は完成です。

# Hello World!
アプリも基盤もそろったのでいよいよWorldにHello!しましょう。
まず、作成したS3のバケットのディレクトリにAngularでビルドした成果物を入れます。
そして、CloudFrontのディストリビューションドメイン名にブラウザからアクセスするとアプリが表示されると思います。
これでHello World!達成です。
# まとめ
最近では、IaCが流行ってきているので大変かもしれませんがCloudFormationのテンプレートでリソースを作っていくのが大事になるのかなと思います。
また、基本的にオプションなどは外しているので実際にお試しいただく時は色々なオプションを付けて試してもらえるといいかなと思います。

# 参考

+ [Amazon CloudFront テンプレートスニペット](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/quickref-cloudfront.html)
+ [CloudFormation の Amazon S3 バケットでカスタムリソースを使用する方法を教えてください。](https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-s3-custom-resources/)
+ [CloudFormation で OAI を使った CloudFront + S3 の静的コンテンツ配信インフラを作る](https://dev.classmethod.jp/articles/s3-cloudfront-with-oai-by-cloudformation/)