AWS Copilot CLIでApp Runnerを作る

# はじめに
少し前に作っていたplumberのコンテナを、今回はAWS Copilot CLIを使って動かしてみました。

https://qiita.com/a_b_/items/32cbc21bd140321719c2

AWS Copilotはサービスのタイプがいくつかありますが、今回は以下の記事を参考にApp Runnerで動かします。

https://aws.amazon.com/jp/builders-flash/202212/apprunner-deploy-copilot-cli/?awsf.filter-name=*all



# やってみた

Cloud9(t3.small)でやってみました。

## IAM の設定
専用のIAMユーザーを作ります。権限は指定がなかったため`AdministratorAccess`を付与しました。
```
touch createCopilotUser.yaml
```
```yaml:createCopilotUser.yaml
AWSTemplateFormatVersion: "2010-09-09"

Resources:
  UserDefine:
    Type: AWS::IAM::User
    Properties:
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess
```
```
aws cloudformation create-stack --stack-name createCopilotUser \
    --template-body file://createCopilotUser.yaml \
    --capabilities CAPABILITY_NAMED_IAM
```
コンソールからアクセスキーを取得しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/dfe89453-15a0-2e9a-8e2e-563b8c3a52db.png)

またCloud9で、認証にtempporary credentialsを使わないように、OFFにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/833d7bc8-6fc7-825f-eaa7-0f338981d55e.png)


あとはそれを公式にあるように`aws configure`で指定します。
```
$ aws configure
AWS Access Key ID [None]: <アクセスキーID>
AWS Secret Access Key [None]: <シークレットアクセスキー>
Default region name [None]: <使用するリージョン>
Default output format [None]: json

$ aws sts get-caller-identity
{
    "Account": <アカウントID>, 
    "UserId": <ユーザーID>, 
    "Arn": "arn:aws:iam::<アカウントID>:user/copilot-user"
}
```


## AWS Copilot のインストール

Linuxなので、以下のコマンドでインストールしました。

```bash
curl -Lo copilot https://github.com/aws/copilot-cli/releases/latest/download/copilot-linux && chmod +x copilot && sudo mv copilot /usr/local/bin/copilot && copilot --help
```

## デプロイするアプリのクローン
こちらは自作のPlumberのアプリを使います。以前の記事を参考に、ディレクトリとファイル作成を行います。

https://qiita.com/a_b_/items/32cbc21bd140321719c2#%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E4%BD%9C%E6%88%90


## AWS Copilot Application を初期化する
初期化コマンドです。

```
copilot app init
```
以下の問いが出てきますので、公式にあるように答えていきました。
```
  What would you like to name your application? [? for help] my-app-runner-application
```

:::note
ここで`copilot init`と打つと、ApplicationとServiceを一緒に作ることになります。
:::

公式と同様、CFnスタックや、パラメータストアが作られていました。
またファイルもつくられていましたので、tree風のコマンドでチェックします。

https://qiita.com/yone098@github/items/bba8a42de6b06e40983b

```
$ pwd;find . | sort | sed '1d;s/^\.//;s/\/\([^/]*\)$/|--\1/;s/\/[^/|]*/|  /g'
/home/ec2-user/environment/test-plumber
|--copilot
|  |--.workspace
|--Dockerfile
|--plumber.R
|--sample.R
```

中身は先ほど指定したアプリケーション名が入っているだけでした。

```:copilot\.workspace
application: my-app-runner-application

```


## AWS Copilot Environment を作成する
以下で環境を作ります。
```
copilot env init
```
以下のように問われるので、公式にあるように答えていきます。

```
  What is your environment's name? [? for help] dev


  Which credentials would you like to use to create dev?  [Use arrows to move, type to filter, ? for more help]
    Enter temporary credentials
  > [profile default]


  Would you like to use the default configuration for a new environment?
    - A new VPC with 2 AZs, 2 public subnets and 2 private subnets
    - A new ECS Cluster
    - New IAM Roles to manage services and jobs in your environment
  [Use arrows to move, type to filter]
  > Yes, use default.
    Yes, but I'd like configure the default resources (CIDR ranges, AZs).
    No, I'd like to import existing resources (VPC, subnets).
```

5分程度で終わりました。
作られたマニュフェストファイルの中身は以下のようになっていました。

<details><summary>クリックで表示</summary><div>

```yaml:copilot/environments/dev/manifest.yml
# The manifest for the "dev" environment.
# Read the full specification for the "Environment" type at:
#  https://aws.github.io/copilot-cli/docs/manifest/environment/

# Your environment name will be used in naming your resources like VPC, cluster, etc.
name: dev
type: Environment

# Import your own VPC and subnets or configure how they should be created.
# network:
#   vpc:
#     id:

# Configure the load balancers in your environment, once created.
# http:
#   public:
#   private:

# Configure observability for your environment resources.
observability:
  container_insights: false

```
</div></details>

次に環境のデプロイです。
```
copilot env deploy
```
複数あれば`--name xxx`で指定するようですが、今回は一つだけなので未指定で動きます。
特に入力は求められず、色々なリソースがつくられます。


## Request-Driven Web Service の作成
Serviceを作ります。
```
copilot svc init
```
入力内容は公式に従いました。
```
  Which service type best represents your service's architecture?  [Use arrows to move, type to filter, ? for more help]
  > Request-Driven Web Service  (App Runner)
    Load Balanced Web Service   (Internet to ECS on Fargate)
    Backend Service             (ECS on Fargate)
    Worker Service              (Events to SQS to ECS on Fargate)
    Static Site                 (Internet to CDN to S3 bucket)


  What do you want to name this service? [? for help] frontend

  Would you like to accept traffic from your environment or the internet?  [Use arrows to move, type to filter, ? for more help]
    Environment         
  > Internet

  Which Dockerfile would you like to use for sampleplumber?  [Use arrows to move, type to filter, ? for more help]
  > ./Dockerfile
    Enter custom path for your Dockerfile
    Use an existing image instead
```

作られたマニュフェストファイルは以下になります。色々なことが設定できそうですが、デフォルトだと最低限になっているようです。

<details><summary>クリックで表示</summary><div>

```yaml:copilot/frontend/manifest.yml
# The manifest for the "frontend" service.
# Read the full specification for the "Request-Driven Web Service" type at:
# https://aws.github.io/copilot-cli/docs/manifest/rd-web-service/

# Your service name will be used in naming your resources like log groups, App Runner services, etc.
name: frontend
# The "architecture" of the service you're running.
type: Request-Driven Web Service

image:
  # Docker build arguments.
  # For additional overrides: https://aws.github.io/copilot-cli/docs/manifest/rd-web-service/#image-build
  build: Dockerfile
  # Port exposed through your container to route traffic to it.
  port: 80

# http:
#   healthcheck:
#     path: /
#     healthy_threshold: 3
#     unhealthy_threshold: 5
#     interval: 10s
#     timeout: 5s

# Number of CPU units for the task.
cpu: 1024
# Amount of memory in MiB used by the task.
memory: 2048

# Connect your App Runner service to your environment's VPC.
# network:
#   vpc:
#     placement: private


# Enable tracing for the service.
# observability:
#   tracing: awsxray

# Optional fields for more advanced use-cases.
#
# variables:                    # Pass environment variables as key value pairs.
#   LOG_LEVEL: info
#
# tags:                         # Pass tags as key value pairs.
#   project: project-name
#
# secrets:                      # Pass secrets from AWS Systems Manager (SSM) Parameter Store and Secrets Manager.
#   GITHUB_TOKEN: GITHUB_TOKEN  # The key is the name of the environment variable, the value is the name of an SSM parameter.

# You can override any of the values defined above by environment.
# environments:
#   test:
#     variables:
#       LOG_LEVEL: debug        # Log level for the "test" environment.
```

</div></details>

これをデプロイします。
```
copilot svc deploy
```
こちらも複数あれば`--name xxxx`で指定しますが、１つだけなので未指定で実行します。
途中の画面ですが、メッセージが赤く出てくるので、少々心臓に悪いです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/ff1a1dcb-2ddc-23e0-c750-4de6af709ace.png)

6分ほど待ち、結果にURLがでてきました
```
Recommended follow-up action:
  - You can access your service at https://xxxxxxxxx.ap-northeast-1.awsapprunner.com over the internet.
```
App RunnerのコンソールでもURLが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/ab0f3580-5dd9-f267-be6d-03f483abcd73.png)

アクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/5ec10470-74d9-9962-706f-1419f74160dc.png)

URLの最後に`/hello`を付けて再アクセスです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/62863f26-47ab-321a-a6e4-10b13f280065.png)


その他、`/hello/jiro/21`の場合。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/d6119648-2602-f37b-7991-6f776bf42f97.png)

`/fn?x=2`の場合。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/f1d8ab1d-ef9c-3e86-8ff0-5c114e466ffa.png)

`/plot`の場合。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/354c5ec5-d01c-736c-9a31-1e7c11cef006.png)


## WAFを付ける

以前App Runnerを作ったときはWAFを付けて作成しました。

https://qiita.com/a_b_/items/03e76bf70e13cbd1bdc8

Copilotでもできるようなのでやってみます。

### 参考

https://dev.classmethod.jp/articles/awscopilotcli-attach-awswaf-to-apprunner/

https://aws.github.io/copilot-cli/ja/blogs/apprunner-waf/

https://aws.github.io/copilot-cli/ja/docs/developing/addons/workload/


### ファイル作成
以下のディレクトリとファイルを追加します。
```
mkdir copilot/frontend/addons
touch copilot/frontend/addons/waf.yml
touch copilot/frontend/addons/addons.parameters.yml
```
WAFを作って、先のApp Runnerに紐づけます。WAFはIP許可リストとしています。
CFnがそのまま使えます。
```yaml:copilot/frontend/addons/waf.yml
#Addon template to add WAF configuration to your App Runner service.

Parameters:
  App:
    Type: String
    Description: Your application's name.
  Env:
    Type: String
    Description: The environment name your service, job, or workflow is being deployed to.
  Name:
    Type: String
    Description: The name of the service, job, or workflow being deployed.
  ServiceARN:
    Type: String
    Default: ""
    Description: The ARN of the service being deployed.
  AllowAddresses:
    Type: CommaDelimitedList
    Default: "1.1.1.1/32,2.2.2.2/32"

Resources:
  WAFIPSet:
    Type: AWS::WAFv2::IPSet
    Properties:
      Name: IPAllowLists
      Scope: REGIONAL
      IPAddressVersion: IPV4
      Addresses: !Ref AllowAddresses
 
  WebACL:
    Type: AWS::WAFv2::WebACL
    Properties:
      Name: WebACL_AppRunner
      Scope: REGIONAL
      DefaultAction:
        Block: {}
      VisibilityConfig:
        SampledRequestsEnabled: true
        CloudWatchMetricsEnabled: true
        MetricName: WebACL_AppRunner
      Rules:
        -
          Name: rules-allow-ip
          Priority: 0
          Action:
            Allow: {}
          Statement:
            IPSetReferenceStatement:
              Arn: !GetAtt WAFIPSet.Arn
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: rules-allow-ip
  
  Firewall:
    Metadata:
      'aws:copilot:description': 'Associating your App Runner service with your WAF WebACL'
    Type: AWS::WAFv2::WebACLAssociation
    Properties: 
      ResourceArn: !Sub ${ServiceARN}
      WebACLArn: !GetAtt WebACL.Arn
```
パラメータは以下のファイルで指定しますが、最初はIPは指定せず`/waf.yml`のデフォルトがセットされるかを見てみます。
```yaml:copilot/frontend/addons/addons.parameters.yml
Parameters:
  ServiceARN: !Ref Service
#   AllowAddresses: ""
```
### デプロイ
デプロイします。先ほどと同じコマンドで、変更にも対応してくれます。
```
copilot svc deploy
```

ちゃんとWAFが作成されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/51e31801-9744-0ae3-e70b-4beffae3b192.png)
デフォルトの値でIP許可リストが作成されました。URLにアクセスしても、見れないことが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/d8570c22-fcdc-945f-78bd-4e9c2ceab3f4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/e0280f17-4b90-2d82-5a99-c7e25b01b907.png)


パラメータを指定してみます。確認くんなどで、自分のIPアドレスを指定します。

```diff_yaml:copilot/frontend/addons/addons.parameters.yml
Parameters:
  ServiceARN: !Ref Service
- #   AllowAddresses: ""
+ AllowAddresses: "自分のIPアドレス/32"
```

```
copilot svc deploy
```

自分のマシンからは見れるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/fa70162b-3ea6-a69d-759c-b5a56f1b4160.png)


## 片付け

`copilot app delete`で、S3バケットも消してくれます。Cloud9内のファイルは`.workspace`ファイルは消されますが、他ファイルは残ったままでした。
```
  Are you sure you want to delete application testcopilot? [? for help] (Y/n) Y
```

最初にIAMユーザを作ったCFnスタックも、必要に応じて削除してください。


# おわりに
今回はAWS Copilot CLIでApp Runnerを試してみました。
以前CFnで作成した時とは異なり、ECRを用意することなく、対話式でよしなに作成できたので、大変楽に作れました。
この記事がどなたかのお役に立てれば幸いです。