Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,9 @@ terraform/status-service/.terraform/providers/registry.terraform.io/hashicorp/ar
terraform/status-service/.terraform/providers/registry.terraform.io/hashicorp/aws/5.36.0/linux_arm64/terraform-provider-aws_v5.36.0_x5
terraform/status-service/.terraform/providers/registry.terraform.io/hashicorp/random/3.6.0/linux_arm64/terraform-provider-random_v3.6.0_x5
terraform/status-service/terraform.tfstate.backup
terraform/compute-node-service/.terraform/providers/registry.terraform.io/hashicorp/archive/2.4.2/linux_arm64/terraform-provider-archive_v2.4.2_x5
terraform/compute-node-service/.terraform/providers/registry.terraform.io/hashicorp/aws/5.38.0/linux_arm64/terraform-provider-aws_v5.38.0_x5
terraform/compute-node-service/.terraform/providers/registry.terraform.io/hashicorp/random/3.6.0/linux_arm64/terraform-provider-random_v3.6.0_x5
terraform/compute-node-service/.terraform.lock.hcl
terraform/compute-node-service/terraform.tfstate.backup
terraform/compute-node-service/tfplan
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@ create-status-service:
docker-compose run app-deploy -cmd create-status-service

destroy-status-service:
docker-compose run app-deploy -cmd destroy-status-service
docker-compose run app-deploy -cmd destroy-status-service

create-compute-node:
docker-compose run app-deploy -cmd create-compute-node

destroy-compute-node:
docker-compose run app-deploy -cmd destroy-compute-node

deploy:
aws ecr get-login-password --profile ${AWS_PROFILE} --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${ACCOUNT}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com
Expand Down
12 changes: 12 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var TerraformAppStateDirectory = "/service/terraform/application-state"
var TerraformGatewayDirectory = "/service/terraform/internet-gateway"
var TerraformApplicationDirectory = "/service/terraform/application-wrapper"
var TerraformStatusServiceDirectory = "/service/terraform/status-service"
var TerraformComputeServiceDirectory = "/service/terraform/compute-node-service"

func main() {
cmdPtr := flag.String("cmd", "plan", "command to execute")
Expand Down Expand Up @@ -83,5 +84,16 @@ func main() {
}
}

// compute node service
if *cmdPtr == "create-compute-node" || *cmdPtr == "destroy-compute-node" {
cmd := exec.Command("/bin/sh", "./scripts/compute-node-service.sh", TerraformComputeServiceDirectory, *cmdPtr)
out, err := cmd.Output()
output := string(out)
fmt.Println(output)
if err != nil {
log.Fatalf("error %s", err.Error())
}
}

log.Println("done")
}
17 changes: 17 additions & 0 deletions scripts/compute-node-service.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/sh

cd $1

if [ $2 = "create-compute-node" ]; then
echo "creating ..."
# terraform validate
echo "running init ..."
export TF_LOG_PATH="error.log"
export TF_LOG=TRACE
terraform init > init.log
terraform plan -out=tfplan > plan.log
terraform apply tfplan
else
echo "deleting ..."
terraform apply -destroy -auto-approve
fi
1 change: 1 addition & 0 deletions terraform/compute-node-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Compute node service
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
from boto3 import client as boto3_client
import boto3
import json
import base64
import os
import time
from botocore.exceptions import ClientError

ecs_client = boto3_client("ecs", region_name=os.environ['REGION'])
iam_resource = boto3.resource("iam")

def lambda_handler(event, context):
cluster_name = os.environ['CLUSTER_NAME']
task_definition_name = os.environ['TASK_DEFINITION_NAME']
container_name = os.environ['CONTAINER_NAME']
security_group = os.environ['SECURITY_GROUP_ID']
subnet_ids = os.environ['SUBNET_IDS']

print(event)

if event['isBase64Encoded'] == True:
body = base64.b64decode(event['body']).decode('utf-8')
event['body'] = body
event['isBase64Encoded'] = False

json_body = json.loads(event['body'])
print(json_body)

account_id = json_body['accountId']
source_url = json_body['sourceUrl']
destination = json_body['destination']

# create user and role
user = None
username = f"deploy-user-{account_id}"
user_key = None

if user_exists(username):
print("user already exists")
else:
try:
try:
user = iam_resource.create_user(UserName=username)
print(f"Created user {user.name}.")
except ClientError as error:
print(
f"Couldn't create a user. Here's why: "
f"{error.response['Error']['Message']}"
)
raise

try:
user_key = user.create_access_key_pair()
print(f"Created access key pair for user.")
except ClientError as error:
print(
f"Couldn't create access keys for user {user.name}. Here's why: "
f"{error.response['Error']['Message']}"
)
raise

time.sleep(10) # wait for user to be created

sts_client = boto3_client(
"sts", aws_access_key_id=user_key.id, aws_secret_access_key=user_key.secret
)

try:
deploy_account_id = sts_client.get_caller_identity()["Account"]
print(f"deploy_account_id: {deploy_account_id}")
assume_role_arn = f"arn:aws:iam::{account_id}:role/ROLE-{deploy_account_id}"

user.create_policy(
PolicyName=f"deploy-user-policy-{account_id}",
PolicyDocument=json.dumps(
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": assume_role_arn,
}
],
}
),
)
print(
f"Created an inline policy for {user.name} that lets the user assume "
f"the role."
)
except ClientError as error:
print(
f"Couldn't create an inline policy for user {user.name}. Here's why: "
f"{error.response['Error']['Message']}"
)
raise

time.sleep(10) # wait
except Exception:
print("something went terribly wrong!")

temp_credentials = None
# Assume role
try:
print("assuming role")
new_sts_client = boto3_client(
"sts", aws_access_key_id=user_key.id, aws_secret_access_key=user_key.secret
)
response = new_sts_client.assume_role(
RoleArn=assume_role_arn, RoleSessionName=f"AssumeRole{account_id}"
)
temp_credentials = response["Credentials"]
print(f"Assumed role {assume_role_arn} and got temporary credentials.")
except ClientError as error:
print(
f"Couldn't assume role {assume_role_arn}. Here's why: "
f"{error.response['Error']['Message']}"
)
raise

# list buckets
# try:
# print("listing buckets")
# s3_resource = boto3.resource(
# "s3",
# aws_access_key_id=temp_credentials["AccessKeyId"],
# aws_secret_access_key=temp_credentials["SecretAccessKey"],
# aws_session_token=temp_credentials["SessionToken"],
# )
# for bucket in s3_resource.buckets.all():
# print(bucket.name)
# except ClientError as error:
# print(
# f"Couldn't list buckets for the account. Here's why: "
# f"{error.response['Error']['Message']}"
# )
# raise

# start Fargate task
if cluster_name != "":
print("Starting Fargate task")
response = ecs_client.run_task(
cluster = cluster_name,
launchType = 'FARGATE',
taskDefinition=task_definition_name,
count = 1,
platformVersion='LATEST',
networkConfiguration={
'awsvpcConfiguration': {
'subnets': subnet_ids.split(","),
'assignPublicIp': 'ENABLED',
'securityGroups': [security_group]
}
},
overrides={
'containerOverrides': [
{
'name': container_name,
'command': ['--context', source_url, "--destination", destination, "--force"],
'environment': [
{
'name': 'AWS_ACCESS_KEY_ID',
'value': temp_credentials["AccessKeyId"]
},
{
'name': 'AWS_SECRET_ACCESS_KEY',
'value': temp_credentials["SecretAccessKey"]
},
{
'name': 'AWS_SESSION_TOKEN',
'value': temp_credentials["SessionToken"]
},
],
},
],
})

return {
'statusCode': 202,
'body': json.dumps(str('Compute node creation initiated'))
}

# TODO: fix error handling
def user_exists(user_name):
try:
iam_resource.get_user(UserName=user_name)
return True
except Exception:
return False
49 changes: 49 additions & 0 deletions terraform/compute-node-service/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// policy document - compute lambda
data "aws_iam_policy_document" "iam_policy_document_compute" {
statement {
sid = "CloudwatchPermissions"
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = ["*"]
}

statement {
sid = "ECSTaskPermissions"
effect = "Allow"
actions = [
"ecs:DescribeTasks",
"ecs:RunTask",
"ecs:ListTasks"
]
resources = ["*"]
}

statement {
sid = "ECSPassRole"
effect = "Allow"
actions = [
"iam:PassRole",
]
resources = [
"*"
]
}

statement {
sid = "IAMPermissions"
effect = "Allow"
actions = [
"iam:CreateUser",
"iam:GetUser",
"iam:CreateAccessKey",
"iam:PutUserPolicy"
]
resources = [
"*"
]
}
}
8 changes: 8 additions & 0 deletions terraform/compute-node-service/ecr.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resource "aws_ecr_repository" "app" {
name = "deploy-app-${random_uuid.val.id}"
image_tag_mutability = "MUTABLE"

image_scanning_configuration {
scan_on_push = false # consider implications of setting to true
}
}
59 changes: 59 additions & 0 deletions terraform/compute-node-service/ecs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// ECS Cluster
resource "aws_kms_key" "ecs_cluster" {
description = "ecs_cluster_kms_key"
deletion_window_in_days = 7
}

resource "aws_cloudwatch_log_group" "ecs_cluster" {
name = "ecs-cluster-log-${random_uuid.val.id}"
}

resource "aws_ecs_cluster" "pipeline_cluster" {
name = "pipeline-cluster-${random_uuid.val.id}"

configuration {
execute_command_configuration {
kms_key_id = aws_kms_key.ecs_cluster.arn
logging = "OVERRIDE"

log_configuration {
cloud_watch_encryption_enabled = true
cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs_cluster.name
}
}
}
}

// ECS Task definition
resource "aws_ecs_task_definition" "pipeline" {
family = "pipeline-${random_uuid.val.id}"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 2048
memory = 4096
task_role_arn = aws_iam_role.task_role_for_ecs_task.arn
execution_role_arn = aws_iam_role.execution_role_for_ecs_task.arn

container_definitions = jsonencode([
{
name = "pipeline-${random_uuid.val.id}"
image = aws_ecr_repository.app.repository_url
essential = true
portMappings = [
{
containerPort = 8081
hostPort = 8081
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = "/ecs/pipeline/${random_uuid.val.id}"
awslogs-region = "us-east-1"
awslogs-stream-prefix = "ecs"
awslogs-create-group = "true"
}
}
}
])
}
Loading