LocalStackを使ってみた(とデモアプリケーションの紹介)

## 背景

* 私たちが提供しているAnyMotionを試せる環境がなかったため、デモアプリケーションを作成しました([こちら](https://demo.anymotion.jp/))。
* デモアプリケーションを作成するにあたり、LocalStackを活用しながら開発を進めたので、LocalStackについて簡単に紹介したいと思います

## LocalStackとは

LocalStackは、シングルコンテナで動作するクラウドサービスエミュレータです。AWSの各種サービスをサポートしており、AWSサービスをローカル環境で動かすことができます。一部のAWSサービスは有償ですが、基本的には無償で使えるサービスが非常に多いです。無償版や有償版で利用できるAWSサービスに関しては、[こちら](https://docs.localstack.cloud/aws/feature-coverage/)をご参照ください。

## 今回やること

1. Terraformを用いて、S3バケットの作成を行う
2. Pythonスクリプトから、S3バケットにファイルをアップロードする


## 実装

### 構成

```sh
.
├── compose.yml
├── infra
│   ├── provider.tf
│   ├── s3.tf
│   └── version.tf
└── src
    ├── __init__.py
    ├── main.py
    └── my_file.txt
```

### 前準備

`compose.yml`を以下のように定義します。環境変数として設定する`AWS_ACCESS_KEY_ID`と`AWS_SECRET_ACCESS_KEY`は適当な値で問題ないです。LocalStackは`4566`ポートを使用するので開けておきます。

```compose.yml
services:
  app:
    image: python:3.9.9
    tty: true
    environment:
      AWS_ACCESS_KEY_ID: dummy
      AWS_SECRET_ACCESS_KEY: dummy
    volumes:
      - ./src:/workspace
    working_dir: /workspace
  
  localstack:
    image: localstack/localstack:0.14.2
    ports:
      - "4566:4566"
```


### S3の作成

Terraformを用いて、S3バケットを作成します。TerraformのバージョンとAWSのバージョンは以下の通りです。

```infra/version.tf
terraform {
  required_version = "1.1.7"
  required_providers {
    aws = {
      version = "4.6.0"
      source  = "hashicorp/aws"
    }
  }
}
```

Terraformの適応先をLocalStackにするために以下のようにproviderを定義します。ここで使用する`access_key`と`secret_key`も適応な値で問題ありません。

```infra/provider.tf
provider "aws" {
  access_key = "dummy"
  secret_key = "dummy"
  region     = "ap-northeast-1"

  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true
  endpoints {
    s3 = "http://s3.localhost.localstack.cloud:4566"
  }
}
```

S3を以下のように作成します。

```infra/s3.tf
resource "aws_s3_bucket" "default" {
  bucket = "my-bucket"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "default" {
  bucket = aws_s3_bucket.default.bucket

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "default" {
  bucket = aws_s3_bucket.default.bucket

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_acl" "default" {
  bucket = aws_s3_bucket.default.id

  acl    = "private"
}
```

`terraform init`と`terraform apply`を実行して、S3バケットが作成されて確認してみましょう。LocalStackのコンテナに入り、S3バケットの一覧を確認します。LocalStackではawsコマンドをラップした`awslocal`コマンドが利用できるため、`awslocal`コマンドを利用して確認を行います。 my-bucketが作成されていることが確認できると思います。

```sh
docker compose up -d
docker compose exec localstack bash
awslocal s3 ls
```


### Pythonコード

AWS SDKをインストールします。
```sh
docker compose exec app bash
pip install boto3
```

ファイルをS3バケットにアップロードします。S3のendpointはLocalStackになるので、endpointを修正します。

```src/main.py
from pathlib import Path

import boto3

BASE_DIR = Path(__file__).parent
BUCKET = "my-bucket"
S3_ENDPOINT = "http://localstack:4566"


def upload(filename: Path) -> None:
    client = boto3.client("s3", endpoint_url=S3_ENDPOINT)
    client.upload_file(str(filename), BUCKET, filename.name)


if __name__ == "__main__":
    filename = BASE_DIR / "my_file.txt"
    upload(filename)
```

ファイルがアップロードされているか確認してみましょう。`awslocal s3 cp s3://my-bucket/my_file.txt .`でローカルに持ってきて、ファイルの中身を確認することもできます。
```sh
docker compose exec localstack bash
awslocal s3 ls s3://my-bucket
awslocal s3 cp s3://my-bucket/my_file.txt .
```


## 話はそれますが(デモアプリケーション)

今回、LocalStackを用いてデモアプリケーションの開発を進めました。デモアプリケーションは[こちら](https://demo.anymotion.jp/)で公開しております。これまで、AnyMotionを手軽に使える環境のありませんでしたので、試しに一度使ってみてください。いくつか制約はありますが、無償でご利用できます。

## まとめ

* LocalStackについて簡単に紹介しました
* LocalStackを用いることにより、ローカル環境での開発を効率よく進めることができます

## 参考

* https://docs.localstack.cloud/integrations/terraform/