【AWS】ECSタスクをLambdaから環境変数追加して起動する方法【Java】

# 最初に
ECSタスクをLambdaから起動するのに手間取ったため、その手法を書き記しておきます。
使用言語はJavaですが、ほかの言語でも似たような実装で実現可能なようです。


## やりたかったこと
* S3にファイルをアップロードしたことをトリガにしてLambdaを起動する
* LambdaからECSタスクを起動する
* アップロードされたファイルをECSでダウンロードする。（そのために、S3のファイル名などを知る必要がありました）
※ Lambdaだけで処理するのが簡単だけど、ストレージ10Gの制限があるためECSを使用することにしました。
![ECS-Lambda-AWS-picture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/d9d9b59b-9117-dfb5-9313-509720fe7e34.png)<br>

## わからなかったこと
1. LambdaからECSタスクを起動する方法
1. 環境変数を指定してECSタスクを起動する方法(S3のファイルパスを指定できない)
1. Javaでの実装方法

## 結論
1. LambdaからECSタスクを起動するには、AWS-SDKの"runTask"関数を使用すればOKでした（[参考にした公式Githubソースコード](https://github.com/awsdocs/aws-doc-sdk-examples/blob/ae164316a5c17957dd9c77d51b14debe2c773323/javav2/example_code/ecs/src/main/java/com/example/ecs/CreateService.java)）
1. runTask関数の引数で指定すればOKでした。ですが、ここがとても面倒です。
1. 下のソースコードを参考にしてください。



## ソースコード
### Lambda側（Java）
Javaで記述する場合、かなり面倒な手順になってしまいます。
参考にしたPythonでの実装例の方が、何をやっているのかがわかりやすいです（[参考にしたサイト](https://dev.classmethod.jp/articles/s3event-trigger-ecstask-on-fargate-bia-lambda/)）。ただ、このサイトの通りにやって一部ハマったので、後述の『ハマったところ』に記載しています。
```Java:Main.java
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
import software.amazon.awssdk.services.ecs.EcsClient;
import com.amazonaws.services.lambda.runtime.Context;
import software.amazon.awssdk.regions.Region;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.S3Event;
import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import software.amazon.awssdk.services.ecs.model.*;

import java.util.ArrayList;
import java.util.List;

public class Main implements RequestHandler<S3Event, String> {
    static String $taskDef ="タスク定義名";
    static String $clusterName ="クラスター名";
    static String $launchType ="FARGATE";
    static String $subnetId ="サブネットID";

    public static void main(String[] args) {
        //ローカル実行用
        mainProcess("ap-northeast-1", "S3のバケット名", "S3のファイルパス");
    }

    // Lambdaで実行される関数
    Gson gson = new GsonBuilder().setPrettyPrinting().create();
    @Override
    public String handleRequest(S3Event event, Context context) {
        // log execution details
        context.getLogger().log("URL files is uploaded at S3 and Lambda is started.");
        try {
            // S3から受け取ったイベントから、必要な情報を収集
            S3EventNotification.S3EventNotificationRecord record = event.getRecords().get(0);
            String awsRegion = record.getAwsRegion();
            String bucketName = record.getS3().getBucket().getName();
            String keyName = record.getS3().getObject().getKey();
            mainProcess(awsRegion, bucketName, keyName);
        } catch (Throwable t) {
            System.out.println(t);
            return "500 NG";
        }
        return "200 OK";
    }

    //実質メイン処理
    static private void mainProcess(String region, String backetName, String keyName) {
        //環境変数をKeyValuePair型で設定
        KeyValuePair kvBacketName = KeyValuePair.builder()
                .name("S3_BUCKETNAME")
                .value(backetName)
                .build();
        KeyValuePair kvKeyName = KeyValuePair.builder()
                .name("S3_KEYNAME")
                .value(keyName)
                .build();
        KeyValuePair kvRegionName = KeyValuePair.builder()
                .name("S3_REGION")
                .value(region)
                .build();
        //リストに格納
        List<KeyValuePair> envValiables = new ArrayList<KeyValuePair>();
        envValiables.add(kvBacketName);
        envValiables.add(kvKeyName);
        envValiables.add(kvRegionName);

        //ContainerOverride型の変数を作って、環境変数のListを埋め込む
        ContainerOverride overrides = ContainerOverride.builder()
                .name("hello")  //ここはコンテナ名と同じにしないと動かないみたいです
                .environment(envValiables)
                .build();

        //TaskOverride型の変数を作って、ContainerOverride型の変数を埋め込む
        TaskOverride taskOverider = TaskOverride.builder()
                .containerOverrides(overrides)
                .build();

        NetworkConfiguration networkConfig = NetworkConfiguration.builder()
                .awsvpcConfiguration(
                        AwsVpcConfiguration.builder()
                                .subnets($subnetId)
                                .assignPublicIp("DISABLED")  //今回はVPCサブネット内でNATを使っているので固定IPを割り当てませんが、そうでない場合はENABLEDにしないとECSが動きません。
                                .build()
                )
                .build();

        //RunTaskRequest.Builder型の変数を作って、Cluster名、Task定義名、そしてTaskOverride型の変数を埋め込む
        RunTaskRequest requests = RunTaskRequest.builder()
                .cluster($clusterName)
                .taskDefinition($taskDef)
                .launchType($launchType)
                .networkConfiguration(networkConfig)
                .overrides(taskOverider)
                .build();

        //ecsにアクセスするためのクライアントを作成
        EcsClient ecs = EcsClient.builder()
                .region(Region.of(region))
                //.credentialsProvider(ProfileCredentialsProvider.create()) //  Gitに載ってたExampleだと使われていたけど、これつけると動かなかった
                .build();

        //ecsタスクを開始する。
        ecs.runTask(requests);

    }
}
```


### 依存関係ファイル（gradle）
結構適当に書いていますがご容赦ください。
```gradle:build.gradle
plugins {
    id 'java'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    //AWS
    implementation 'software.amazon.awssdk:ecs:2.17.209'
    implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
    implementation 'com.amazonaws:aws-java-sdk-lambda:1.12.237'
    implementation group: 'com.amazonaws', name: 'aws-lambda-java-events', version: '3.11.0'
    //外部ライブラリ
    implementation 'com.google.code.gson:gson:2.8.6'
    implementation 'org.slf4j:slf4j-nop:1.7.36'
    //テスト用
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

test {
    useJUnitPlatform()
}
```

### ECS側（Java）
テスト用にECSで動かすプログラムです。環境変数の一覧を表示するだけです。
```Java:ContainerSide.java
    public static void containerSide (String[] args) {
        System.out.println("Hello Environment Variables!!");
        Map<String,String> envMap = System.getenv();
        System.out.println( envMap );
        // 環境変数の一覧を出力
        for( String key : envMap.keySet() ) {
            String value = envMap.get(key);
            System.out.println( key + " : " + value );
        }
    }

```


### 結果
ECSの実行ログです。
Lambdaから実行して、みごと環境変数が設定されたうえで実行されていました。
![ECS-Lambda-result-picture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/6eb026a5-4fe5-8616-c400-05f277026fd6.png)

### その他情報
* ECSの「サービス」ではなくて「タスク」を起動する理由は、1回実行したら即終了してほしいからです。ECSのサービスを使う場合は、もう少し面倒な設定をしないとタスクが永遠に再実行され続けます。
* ECSにおける「クラスター」「サービス」「タスク」「タスク定義」の違いについては[【こちらのYoutube動画】](https://www.youtube.com/watch?v=Bq-DT30hesA)がとても参考になります）

## ハマったこと
### LambdaのIAMロール設定
IAMロール設定をDefaultにして、参考サイトの通りに実装しても「権限がない」と怒られ続けた。
調べてみたら、デフォルトでつける権限だとECRのリポジトリを更新したら使えなくなったり、そもそもTask実行の権限が無かったり。
[こちらのサイト](https://soypocket.com/it/ecs-task-schedule-taskdefinition-latest/)を参考にしました。

### name属性の設定
Lambdaのソースコード中に"ContainerOverride"型Build時の"name"属性がありますが、ここには使用するコンテナ名を入力しないといけないみたいでした。
コンテナ名は、ECS->タスク定義の画面から使用するタスク定義をクリックして確認することができます。
![ECS-Lambda-containerName-picture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2618997/5da2f3a0-293f-6261-c2e4-e7bbac6e9a9c.png)

### Credentialの設定
ECSのクライアント作成時に公式のGitに書いてあるコードをまるまるコピりましたが、これでは動かないようです。credentialsProviderを使わなければデフォルトの設定が反映されるようなので、コメントアウトしたら動くようになりました。
```java
    //ecsにアクセスするためのクライアントを作成
    EcsClient ecs = EcsClient.builder()
        .region(Region.of(region))
        .credentialsProvider(ProfileCredentialsProvider.create()) //  ←これ
       .build();
```


## 参考にしたサイト
・一番参考になったサイト。Pythonでの実装だけど、やってることは同じです。

https://dev.classmethod.jp/articles/s3event-trigger-ecstask-on-fargate-bia-lambda/

・別に今回みたいな実装をしなくても、このサイトみたいにする方法もありかと。

https://blog.denet.co.jp/cloudformation-ecs-setenv/

・公式APIドキュメント。Pythonのサイトとこのドキュメントを参考にしながら実装しました。

https://sdk.amazonaws.com/java/api/latest/

・公式Githubのサンプルソース。立ち上げているのはECSのサービスだけど、十分参考になりました。ほかにも、SDK実装でわからないことがあったら、ここを覗くと解決したりします。

https://github.com/awsdocs/aws-doc-sdk-examples/blob/ae164316a5c17957dd9c77d51b14debe2c773323/javav2/example_code/ecs/src/main/java/com/example/ecs/CreateService.java

・ソース中の”name”属性が必要だと教えてくれたサイト

https://github.com/aws/aws-sdk-go/issues/2553

・IAMロールについてのエラー対応サイト。こんな罠があってたまるか！！

https://soypocket.com/it/ecs-task-schedule-taskdefinition-latest/


## 最後に
ECS側がJavaで動くので、それに合わせてJavaで実装しました。
確認してはいませんが、ほかのLambda対応言語（RubyやGo）でも同じようなアプローチで、環境変数を指定してECSタスクの実行が可能かと思われます。